feat(ai): support customize chat title

This commit is contained in:
Innei
2025-08-07 20:57:12 +08:00
parent c654ca176c
commit cb5a2cd114
8 changed files with 400 additions and 80 deletions
+1 -1
View File
@@ -1 +1 @@
Before you start, you need to read and follow the rules in @claude.md
Before you start, you need to read and follow the rules in @../claude.md
+16 -72
View File
@@ -13,6 +13,8 @@ Follow (Folo) is a modern RSS aggregator and content management platform with mu
- **`apps/desktop/`** - Electron desktop application
- `layer/main/` - Electron main process
- `layer/renderer/` - Vite + React renderer (primary web app)
- `plugins/` - Custom Electron plugins
- `resources/` - Application resources and assets
- **`apps/mobile/`** - React Native mobile app for iOS and Android
- `src/` - React Native source code
- `native/` - Native module implementations
@@ -41,6 +43,16 @@ Follow (Folo) is a modern RSS aggregator and content management platform with mu
- `tailwindcss/` - Tailwind CSS configurations
- TypeScript and build configurations
- **`packages/readability/`** - Content readability parsing and extraction
- **`packages/changelog-cli/`** - CLI tool for changelog generation
### Supporting Directories
- **`scripts/`** - Build and development scripts
- **`plugins/`** - Custom development plugins
- **`locales/`** - Internationalization files organized by feature
- **`icons/`** - Icon assets (MingCute and custom icons)
- **`.github/`** - GitHub workflows, issue templates, and CI/CD
- **`patches/`** - Package patches for dependency modifications
## Essential Commands
@@ -108,45 +120,9 @@ pnpm run test
- **Tailwind CSS** for styling across all platforms
- Platform-specific Tailwind configs in each app
- Shared Tailwind utilities in `packages/configs/tailwindcss/`
- **UI Design Style**: Follow Vercel and Linear SaaS UI aesthetics - clean, modern, minimal design with subtle shadows, rounded corners, and excellent typography
#### UIKit Colors for Desktop Components
For desktop components (`apps/desktop/**/*`) and shared UI components (`packages/internal/components/**/*`), use Apple UIKit color system with Tailwind classes. **Important**: Always use the correct Tailwind prefix for each color category:
**System Colors**: `text-red`, `bg-red`, `border-red` (same for `orange`, `yellow`, `green`, `mint`, `teal`, `cyan`, `blue`, `indigo`, `purple`, `pink`, `brown`, `gray`)
**Fill Colors**:
- Background: `bg-fill`, `bg-fill-secondary`, `bg-fill-tertiary`, `bg-fill-quaternary`, `bg-fill-quinary`, `bg-fill-vibrant`, `bg-fill-vibrant-secondary`, `bg-fill-vibrant-tertiary`, `bg-fill-vibrant-quaternary`, `bg-fill-vibrant-quinary`
- Border: `border-fill`, `border-fill-secondary`, etc.
**Text Colors**: `text-text`, `text-text-secondary`, `text-text-tertiary`, `text-text-quaternary`, `text-text-quinary`, `text-text-vibrant`, `text-text-vibrant-secondary`, `text-text-vibrant-tertiary`, `text-text-vibrant-quaternary`, `text-text-vibrant-quinary`
**Material Colors**: `bg-material-ultra-thick`, `bg-material-thick`, `bg-material-medium`, `bg-material-thin`, `bg-material-ultra-thin`, `bg-material-opaque`
**Control Colors**: `bg-control-enabled`, `bg-control-disabled`
**Interface Colors**: `bg-menu`, `bg-popover`, `bg-titlebar`, `bg-sidebar`, `bg-selection-focused`, `bg-selection-focused-fill`, `bg-selection-unfocused`, `bg-selection-unfocused-fill`, `bg-header-view`, `bg-tooltip`, `bg-under-window-background`
These colors automatically adapt to light/dark mode following Apple's design system. Remember to use the appropriate prefix (`text-`, `bg-`, `border-`) based on the CSS property you're styling.
#### Icons
For icon usage, prioritize the MingCute icon library with the `i-mgc-` prefix. Icons are available in the format `i-mgc-[icon-name]-[style]` where style can be `re` (regular), `fi` (filled), etc.
**Important**: Always try to find an appropriate icon with the `i-mgc-` prefix first. Only use the `i-mingcute-` prefix as a fallback when no suitable `i-mgc-` icon exists.
Examples:
- Preferred: `i-mgc-copy-cute-re`, `i-mgc-external-link-cute-re`
- Fallback only: `i-mingcute-copy-line` (only if no mgc equivalent exists)
### Database
- **Drizzle ORM** with SQLite for local data storage
- Platform-specific database implementations in `packages/internal/database/`
- Migration system with versioned SQL files
For desktop, see @apps/desktop/CLAUDE.md
For mobile, see @apps/mobile/CLAUDE.md
### Internationalization
@@ -172,25 +148,6 @@ Example:
}
```
## Development Setup Notes
### Prerequisites
- Enable Corepack: `corepack enable && corepack prepare`
- For mobile development: macOS with Xcode required
### Environment Setup
- Copy `.env.example` to `.env` in relevant app directories
- For development, use `VITE_API_URL=https://api.follow.is`
- Browser development uses `https://app.folo.is/__debug_proxy` for API access
### Mobile Development
- iOS development requires Xcode and iOS simulator
- Native module development in `apps/mobile/native/`
- Use `pnpm expo prebuild --clean` for clean builds
## Code Organization Patterns
### Import Conventions
@@ -204,26 +161,13 @@ Example:
- Shared UI components in `packages/internal/components/`
- Platform-specific components in respective app directories
- Use TypeScript interfaces for component props
- **Detailed guidelines**: See @packages/internal/CLAUDE.md for component development patterns
### Error Handling
- Custom error parsing utilities in `packages/internal/utils/`
- Sentry integration for error reporting across platforms
## Build and Deployment
### Build Outputs
- Desktop: `apps/desktop/out/` for packaged applications
- Web: `apps/desktop/out/web/` for static web assets
- Mobile: Platform-specific build directories
### Turbo Configuration
- Build orchestration via `turbo.json`
- Dependency-aware builds with output caching
- Parallel execution for independent packages
## Testing Strategy
- Vitest for unit testing across packages
@@ -354,7 +298,7 @@ Claude Code has access to specialized agents for different development tasks. **
## Agent Usage Notes
- **Primary web app is at `@apps/desktop/layer/renderer`** - A Vite + React SPA, not full Electron features
- **Primary web app is at `apps/desktop/layer/renderer`** - A Vite + React SPA, not full Electron features
- **SSR app is minimal** - Only for external sharing pages, not main development focus
- **Mobile app** - React Native with Expo for iOS/Android
- **Always use Task tool with appropriate agent** - Don't implement complex features directly, delegate to specialized agents
+116
View File
@@ -0,0 +1,116 @@
# Desktop App Development Guide
This file provides specific guidance for developing the Electron desktop application.
## Architecture
- **Main Process** (`layer/main/`) - Electron main process handling system integration
- **Renderer Process** (`layer/renderer/`) - Vite + React renderer (primary web app)
The renderer is the **primary web application** - a Vite + React SPA that can run both in Electron and as a standalone web app.
## UI Style
- **UI Design Style**: Follow Vercel and Linear SaaS UI aesthetics - clean, modern, minimal design with subtle shadows, rounded corners, and excellent typography
- **Tailwind CSS** for styling across all platforms
- Platform-specific Tailwind configs in each app
## Development Commands
```bash
# Recommended: Browser development (faster)
pnpm run dev:web
# Full Electron development
pnpm run dev:electron
# Build web version
pnpm run build:web
```
## UIKit Colors for Desktop Components
For desktop components (`apps/desktop/**/*`) and shared UI components (`packages/internal/components/**/*`), use Apple UIKit color system with Tailwind classes. **Important**: Always use the correct Tailwind prefix for each color category:
**System Colors**: `text-red`, `bg-red`, `border-red` (same for `orange`, `yellow`, `green`, `mint`, `teal`, `cyan`, `blue`, `indigo`, `purple`, `pink`, `brown`, `gray`)
**Fill Colors**:
- Background: `bg-fill`, `bg-fill-secondary`, `bg-fill-tertiary`, `bg-fill-quaternary`, `bg-fill-quinary`, `bg-fill-vibrant`, `bg-fill-vibrant-secondary`, `bg-fill-vibrant-tertiary`, `bg-fill-vibrant-quaternary`, `bg-fill-vibrant-quinary`
- Border: `border-fill`, `border-fill-secondary`, etc.
**Text Colors**: `text-text`, `text-text-secondary`, `text-text-tertiary`, `text-text-quaternary`, `text-text-quinary`, `text-text-vibrant`, `text-text-vibrant-secondary`, `text-text-vibrant-tertiary`, `text-text-vibrant-quaternary`, `text-text-vibrant-quinary`
**Material Colors**: `bg-material-ultra-thick`, `bg-material-thick`, `bg-material-medium`, `bg-material-thin`, `bg-material-ultra-thin`, `bg-material-opaque`
**Control Colors**: `bg-control-enabled`, `bg-control-disabled`
**Interface Colors**: `bg-menu`, `bg-popover`, `bg-titlebar`, `bg-sidebar`, `bg-selection-focused`, `bg-selection-focused-fill`, `bg-selection-unfocused`, `bg-selection-unfocused-fill`, `bg-header-view`, `bg-tooltip`, `bg-under-window-background`
These colors automatically adapt to light/dark mode following Apple's design system. Remember to use the appropriate prefix (`text-`, `bg-`, `border-`) based on the CSS property you're styling.
## Icons
For icon usage, prioritize the MingCute icon library with the `i-mgc-` prefix. Icons are available in the format `i-mgc-[icon-name]-[style]` where style can be `re` (regular), `fi` (filled), etc.
**Important**: Always try to find an appropriate icon with the `i-mgc-` prefix first. Only use the `i-mingcute-` prefix as a fallback when no suitable `i-mgc-` icon exists.
Examples:
- Preferred: `i-mgc-copy-cute-re`, `i-mgc-external-link-cute-re`
- Fallback only: `i-mingcute-copy-line` (only if no mgc equivalent exists)
## Animation with Framer Motion
- **LazyMotion Integration**: Project uses Framer Motion with LazyMotion for optimized bundle size
- **Usage Rule**: Always use `m.` instead of `motion.` when creating animated components
- **Import**: `import { m } from 'motion/react'`
- **Examples**: `m.div`, `m.button`, `m.span` (not `motion.div`, `motion.button`, etc.)
- **Benefits**: Reduces bundle size while maintaining all Framer Motion functionality
**Animation Presets**:
- **Prefer Spring Presets**: Use predefined spring animations from `@follow/components/constants/spring.js`
- **Available Presets Constants**: `Spring.presets.smooth`, `Spring.presets.snappy`, `Spring.presets.bouncy` (extracted from Apple's spring parameters)
- **Usage Example**: `transition={Spring.presets.smooth}` or `transition={Spring.snappy(0.3, 0.1)}`
- **Customization**: All presets accept optional `duration` and `extraBounce` parameters
## Reusable UI Components
When building UI components, follow this hierarchy:
1. **Check Existing Components First**: Look in `apps/desktop/layer/renderer/src/modules/renderer/components` for app-specific components
2. **Create Reusable Components**: If the component doesn't exist and is **generic/reusable** (not tied to specific app business logic), create it in `packages/internal/components`
### Guidelines for Reusable Components (`packages/internal/components`)
- **Purpose**: Components should be generic and reusable across different apps/contexts
- **No Business Logic**: Avoid coupling with specific app business logic, APIs, or state management
- **Follow All Style Rules**: Must adhere to UIKit colors, MingCute icons (`i-mgc-` prefix), and Framer Motion (`m.` prefix) guidelines
- **Export Pattern**: Export components from appropriate index files for clean imports
- **TypeScript**: Use proper TypeScript interfaces for props and maintain type safety
**Example Structure**:
```
packages/internal/components/
├── ui/ # Basic UI primitives (Button, Input, Modal)
├── layout/ # Layout components (Grid, Stack, Container)
├── feedback/ # User feedback (Toast, Loading, Alert)
└── index.ts # Main exports
```
**Import Examples**:
```tsx
// Correct - from shared components
import { Button, Modal } from "@follow/components"
// App-specific components stay in name/components
import { FeedList } from "~/modules/name/components"
```
## Build Outputs
- Desktop: `apps/desktop/out/` for packaged applications
- Web: `apps/desktop/out/web/` for static web assets
@@ -7,6 +7,7 @@ import { useChatActions, useCurrentTitle } from "~/modules/ai-chat/store/hooks"
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
import { ChatMoreDropdown } from "./ChatMoreDropdown"
import { EditableTitle } from "./EditableTitle"
export const ChatHeader = () => {
const currentTitle = useCurrentTitle()
@@ -32,6 +33,13 @@ export const ChatHeader = () => {
})
}, [chatActions, currentTitle, ask, t])
const handleTitleSave = useCallback(
async (newTitle: string) => {
await chatActions.editChatTitle(newTitle)
},
[chatActions],
)
const maskImage = `linear-gradient(to bottom, black 0%, black 75%, transparent 100%)`
return (
<div className="absolute inset-x-0 top-0 z-[1] h-12">
@@ -46,13 +54,11 @@ export const ChatHeader = () => {
<div className="relative z-10 flex h-full items-center justify-between px-4">
{/* Left side - Title */}
<div className="mr-2 min-w-0 flex-1">
{currentTitle && (
<h1 key={currentTitle} className="text-text truncate font-bold">
<span className="animate-mask-left-to-right [--animation-duration:1s]">
{currentTitle}
</span>
</h1>
)}
<EditableTitle
title={currentTitle}
onSave={handleTitleSave}
placeholder="Untitled Chat"
/>
</div>
{/* Right side - Actions */}
@@ -0,0 +1,139 @@
import { Input } from "@follow/components/ui/input/Input.js"
import { cn } from "@follow/utils/utils"
import { useCallback, useEffect, useState } from "react"
interface EditableTitleProps {
title?: string
onSave: (newTitle: string) => Promise<void>
className?: string
placeholder?: string
maxLength?: number
}
export const EditableTitle = ({
title = "",
onSave,
className,
placeholder = "Untitled Chat",
maxLength = 100,
}: EditableTitleProps) => {
const [isEditing, setIsEditing] = useState(false)
const [editValue, setEditValue] = useState("")
const [isSaving, setIsSaving] = useState(false)
const [originalTitle, setOriginalTitle] = useState("")
const [saveSuccess, setSaveSuccess] = useState(false)
const displayTitle = title || placeholder
const handleStartEdit = useCallback(() => {
setOriginalTitle(title)
setEditValue(title)
setIsEditing(true)
}, [title])
const handleSave = useCallback(async () => {
if (isSaving) return
const trimmedValue = editValue.trim()
// If no changes, just exit edit mode
if (trimmedValue === originalTitle) {
setIsEditing(false)
return
}
try {
setIsSaving(true)
await onSave(trimmedValue)
// Show success feedback briefly
setSaveSuccess(true)
setTimeout(() => setSaveSuccess(false), 2000)
setIsEditing(false)
} catch (error) {
console.error("Failed to save title:", error)
// Keep in edit mode on error to allow retry
} finally {
setIsSaving(false)
}
}, [editValue, originalTitle, onSave, isSaving])
const handleCancel = useCallback(() => {
setEditValue(originalTitle)
setIsEditing(false)
setIsSaving(false)
}, [originalTitle])
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault()
handleSave()
} else if (event.key === "Escape") {
event.preventDefault()
handleCancel()
}
},
[handleSave, handleCancel],
)
const handleBlur = useCallback(() => {
if (!isSaving) {
handleSave()
}
}, [handleSave, isSaving])
// Auto-focus input when entering edit mode
useEffect(() => {
if (isEditing) {
const input = document.activeElement as HTMLInputElement
if (input?.select) {
input.select()
}
}
}, [isEditing])
if (isEditing) {
return (
<div className="relative min-w-0 flex-1">
<Input
ref={(ref) => ref?.focus()}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
maxLength={maxLength}
disabled={isSaving}
className={cn(
"text-text border-accent/50 focus:border-accent bg-transparent font-bold",
"h-auto p-2 text-base leading-none",
isSaving && "cursor-not-allowed opacity-60",
className,
)}
placeholder={placeholder}
/>
{isSaving && (
<div className="animate-in fade-in zoom-in-75 absolute right-2 top-1/2 -translate-y-1/2 duration-200">
<i className="i-mgc-loading-3-cute-re text-text-secondary size-4 animate-spin" />
</div>
)}
</div>
)
}
return (
<div
className={cn("group flex min-w-0 flex-1 cursor-pointer items-center gap-2", className)}
onClick={handleStartEdit}
>
<h1 className="text-text truncate font-bold transition-opacity group-hover:opacity-80">
<span className="animate-mask-left-to-right [--animation-duration:1s]">{displayTitle}</span>
</h1>
{saveSuccess && (
<i className="i-mgc-check-cute-re text-green animate-in fade-in zoom-in-50 size-4 transition-all duration-300" />
)}
<i className="i-mgc-edit-cute-re text-text-secondary group-hover:animate-in group-hover:fade-in group-hover:zoom-in-75 size-4 opacity-0 transition-all duration-150 hover:opacity-100 group-hover:opacity-60" />
</div>
)
}
@@ -92,6 +92,35 @@ export class ChatSliceActions {
return this.get().chatId
}
// Edit chat title
editChatTitle = async (newTitle: string) => {
const currentChatId = this.getCurrentChatId()
if (!currentChatId) {
throw new Error("No active chat to edit title for")
}
const trimmedTitle = newTitle.trim()
const currentTitle = this.getCurrentTitle()
// If no changes, return early
if (trimmedTitle === currentTitle) {
return
}
try {
// Optimistic update
this.setCurrentTitle(trimmedTitle)
// Persist to database
await AIPersistService.updateSessionTitle(currentChatId, trimmedTitle)
} catch (error) {
// Rollback on error
this.setCurrentTitle(currentTitle)
console.error("Failed to update chat title:", error)
throw error
}
}
// Core chat actions using AI SDK AbstractChat methods
sendMessage = async (message: string | BizUIMessage) => {
try {
+44
View File
@@ -0,0 +1,44 @@
# Mobile App Development Guide
This file provides specific guidance for developing the React Native mobile application.
## Architecture
- **React Native** with Expo for cross-platform mobile development
- **Native modules** in `native/` for platform-specific functionality
- **Web views** in `web-app/` for HTML rendering within the mobile app
- **Shared components** with the desktop app through `packages/internal/`
## Development Commands
```bash
# Start Expo development server
pnpm run dev
# Start with specific platform
pnpm run ios
pnpm run android
# Clean rebuild (useful for native module changes)
pnpm expo prebuild --clean
```
## UIKit Colors for React Native Components
For react native components (`apps/mobile/**/*`), use Apple UIKit color system with Tailwind classes. **Important**: Always use the correct Tailwind prefix for each color category:
### System Colors
- Background: `system-background`, `secondary-system-background`, `tertiary-system-background`
- Grouped: `system-grouped-background`, `secondary-system-grouped-background`, `tertiary-system-grouped-background`
- Labels: `label`, `secondary-label`, `tertiary-label`, `quaternary-label`
- Fills: `system-fill`, `secondary-system-fill`, `tertiary-system-fill`, `quaternary-system-fill`
- Separators: `separator`, `opaque-separator`, `non-opaque-separator`
### Semantic Colors
- Core: `red`, `orange`, `yellow`, `green`, `mint`, `teal`, `cyan`, `blue`, `indigo`, `purple`, `pink`, `brown`
- Grays: `gray`, `gray2`, `gray3`, `gray4`, `gray5`, `gray6`
- Interactive: `link`, `placeholder-text`
These colors automatically adapt to light/dark mode following Apple's design system. Remember to use the appropriate prefix (`text-`, `bg-`, `border-`) based on the CSS property you're styling.
+42
View File
@@ -0,0 +1,42 @@
# Internal Packages Development Guide
This file provides specific guidance for developing shared packages used across all platforms.
## Overview
The `packages/internal/` directory contains core shared packages that provide common functionality across desktop, mobile, and SSR applications.
## Package Structure
- `atoms/` - Jotai atomic state definitions
- `components/` - Shared UI components
- `constants/` - Application constants
- `database/` - Drizzle ORM database layer
- `hooks/` - Shared React hooks
- `models/` - Data models and schemas
- `shared/` - Cross-platform shared utilities
- `store/` - Zustand stores
- `types/` - TypeScript type definitions
- `utils/` - Utility functions and helpers
- `tracker/` - Analytics and tracking
- `logger/` - Logging utilities
- `legal/` - Legal and compliance utilities
## State Management
- **Jotai** for atomic state management across all platforms
- **Zustand** for complex state stores (in `packages/internal/store/`)
- **React Query** for server state management
## Database
- **Drizzle ORM** with SQLite for local data storage
- Platform-specific database implementations in `packages/internal/database/`
- Migration system with versioned SQL files
## Component Development Guidelines
- Shared UI components in `packages/internal/components/`
- Platform-specific components in respective app directories
- Use TypeScript interfaces for component props
- Follow cross-platform compatibility patterns