Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"github.copilot-chat"
Comment on lines +4 to +5
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding github.copilot-chat to the recommended extensions is unrelated to the home page refactoring described in the PR. This change should ideally be in a separate PR or the PR description should mention it. However, this is a minor organizational issue and doesn't affect functionality.

Suggested change
"rust-lang.rust-analyzer",
"github.copilot-chat"
"rust-lang.rust-analyzer"

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不需要关注

]
}
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

251 changes: 251 additions & 0 deletions src/pages/home/components/ai-chat-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { useState, useRef, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Sparkles,
Send,
Loader2,
User,
Bot,
MessageSquare,
} from "lucide-react";
import { cn } from "@/lib/utils";

interface Message {
id: string;
role: "user" | "assistant";
content: string;
timestamp: Date;
}

const INITIAL_MESSAGES: Message[] = [
{
id: "1",
role: "assistant",
content: "你好!我是你的 AI 助手。有什么我可以帮助你的吗?",
timestamp: new Date(),
},
];

export function AiChatBox() {
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);

// Auto scroll to bottom when new messages arrive
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);

const handleSend = async () => {
if (!input.trim() || isLoading) return;

const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: input.trim(),
timestamp: new Date(),
};

setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);

// Simulate AI response
setTimeout(() => {
const aiMessage: Message = {
id: (Date.now() + 1).toString(),
Comment on lines +50 to +63
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message ID generation using Date.now().toString() could potentially create duplicate IDs if two messages are created in rapid succession within the same millisecond. Consider using a more robust ID generation method such as crypto.randomUUID() or a counter-based approach to ensure uniqueness.

Copilot uses AI. Check for mistakes.
role: "assistant",
content: `我收到了你的消息:"${userMessage.content}"。这是一个模拟回复。`,
timestamp: new Date(),
};
setMessages((prev) => [...prev, aiMessage]);
setIsLoading(false);
}, 1000);
};
Comment on lines +60 to +71
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AI chat response is simulated with a setTimeout and returns a mock message. While acceptable for demonstration, this should be clearly documented or replaced with actual AI integration to avoid confusion.

Copilot uses AI. Check for mistakes.

const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};

return (
<Card className="overflow-hidden border-border/50 bg-background hover:shadow-lg transition-shadow duration-300">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<div className="p-1.5 bg-primary/10 rounded-lg">
<Sparkles size={18} className="text-primary" />
</div>
<span className="font-semibold text-foreground">AI 助手</span>
</CardTitle>
<p className="text-xs text-muted-foreground mt-0.5">
随时为你提供帮助和建议
</p>
</CardHeader>

<CardContent className="space-y-3 px-6 pb-4">
{/* Messages Area */}
<ScrollArea
className="h-40 rounded-lg border border-border/50 bg-muted/20 p-3"
ref={scrollRef}
Comment on lines +96 to +98
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ScrollArea component is being used with a ref prop, but the ScrollArea component from shadcn/ui doesn't forward refs by default. The ref should be passed to the viewport element inside ScrollArea using ScrollAreaViewport or a custom ref handling mechanism. This will cause the auto-scroll functionality to not work properly.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing accessibility attribute. The scrollable area should have an accessible label or aria-label to improve screen reader support for users navigating the chat messages.

Suggested change
ref={scrollRef}
ref={scrollRef}
aria-label="AI 聊天消息历史"

Copilot uses AI. Check for mistakes.
>
<div className="space-y-4">
{messages.map((message) => (
<MessageBubble key={message.id} message={message} />
))}
{isLoading && (
<div className="flex items-start gap-3">
<Avatar className="h-8 w-8 border border-border">
<AvatarFallback className="bg-primary/10">
<Bot size={16} className="text-primary" />
</AvatarFallback>
</Avatar>
<div className="flex items-center gap-2 bg-muted rounded-2xl rounded-tl-sm px-4 py-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-sm text-muted-foreground">
正在思考...
</span>
</div>
</div>
)}
</div>
</ScrollArea>

{/* Input Area */}
<div className="flex gap-2">
<div className="relative flex-1">
<Input
placeholder="输入你的问题..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
disabled={isLoading}
className="pr-10 border-border/50"
/>
<MessageSquare
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
size={16}
/>
</div>
<Button
onClick={handleSend}
disabled={!input.trim() || isLoading}
size="icon"
aria-label="发送消息"
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send size={16} />
)}
</Button>
</div>

{/* Quick Actions */}
<div className="flex flex-wrap gap-2">
<QuickActionButton
text="总结笔记"
onClick={() => setInput("帮我总结最近的笔记")}
/>
<QuickActionButton
text="创建待办"
onClick={() => setInput("帮我创建一个新的待办事项")}
/>
<QuickActionButton
text="搜索内容"
onClick={() => setInput("帮我搜索相关内容")}
/>
</div>
Comment on lines +153 to +166
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick action buttons set input values but don't automatically submit. This may create confusion as users might expect these buttons to immediately execute the action rather than just populating the input field.

Copilot uses AI. Check for mistakes.
</CardContent>
</Card>
);
}

interface MessageBubbleProps {
message: Message;
}

function MessageBubble({ message }: MessageBubbleProps) {
const isUser = message.role === "user";

return (
<div
className={cn(
"flex items-start gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300",
isUser && "flex-row-reverse"
)}
>
<Avatar
className={cn(
"h-8 w-8 border flex-shrink-0",
isUser ? "border-border bg-primary/10" : "border-border bg-muted"
)}
>
<AvatarFallback
className={cn(
isUser
? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground"
)}
>
{isUser ? <User size={16} /> : <Bot size={16} />}
</AvatarFallback>
</Avatar>

<div
className={cn(
"rounded-2xl px-3 py-2 max-w-[80%]",
isUser
? "bg-primary text-primary-foreground rounded-tr-sm"
: "bg-muted rounded-tl-sm"
)}
>
<p
className={cn(
"text-sm leading-relaxed",
isUser ? "text-primary-foreground" : "text-foreground"
)}
>
{message.content}
</p>
<span
className={cn(
"text-xs mt-1 block",
isUser ? "text-primary-foreground/80" : "text-muted-foreground"
)}
>
{message.timestamp.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
</div>
);
}

interface QuickActionButtonProps {
text: string;
onClick: () => void;
}

function QuickActionButton({ text, onClick }: QuickActionButtonProps) {
return (
<Button
variant="outline"
size="sm"
onClick={onClick}
className="text-xs border-border/50 hover:border-primary transition-colors"
>
{text}
</Button>
);
}
95 changes: 62 additions & 33 deletions src/pages/home/components/home-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,72 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { ChevronDown, Plus, Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export function HomeHeader() {
const [searchQuery, setSearchQuery] = useState("");

const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
// TODO: 实现搜索逻辑
console.log("Search:", searchQuery);
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The console.log statement in the handleSearch function should be removed before merging to production. Console logs in event handlers are typically debugging artifacts that should not be left in production code.

Suggested change
console.log("Search:", searchQuery);

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

暂时不需要关注

Comment on lines 11 to +19
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search functionality is not implemented (only logs to console). The TODO comment indicates this, but a placeholder that doesn't perform actual search functionality may confuse users if they interact with it.

Suggested change
export function HomeHeader() {
const [searchQuery, setSearchQuery] = useState("");
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
// TODO: 实现搜索逻辑
console.log("Search:", searchQuery);
export function HomeHeader({ onSearch }: { onSearch?: (query: string) => void }) {
const [searchQuery, setSearchQuery] = useState("");
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (onSearch) {
onSearch(searchQuery);
}

Copilot uses AI. Check for mistakes.
};

return (
<>
<header className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8">
<div></div>
<div className="flex items-center gap-2 mt-4 sm:mt-0">
<div className="relative">
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
size={18}
/>
<Button variant="outline" className="pl-10 w-40 md:w-64">
搜索...
<header className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-8">
{/* Left side: Empty for future use */}
<div></div>

{/* Right side: Search and Create */}
<div className="flex items-center gap-3 w-full sm:w-auto">
{/* Search */}
<form onSubmit={handleSearch} className="flex-1 sm:flex-none relative">
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
size={18}
/>
<Input
placeholder="搜索笔记..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 w-full sm:w-64 h-10"
/>
</form>

{/* Create dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2">
<Plus size={18} />
新建
<ChevronDown size={16} />
</Button>
{/* <Input placeholder="搜索..." className="pl-10 w-40 md:w-64" /> */}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<Plus size={16} className="mr-2" /> 新建{" "}
<ChevronDown size={16} className="ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>空白笔记</DropdownMenuItem>
<DropdownMenuItem>设置Todo</DropdownMenuItem>
<DropdownMenuItem>从模板创建...</DropdownMenuItem>
<Separator />
<DropdownMenuItem>新的笔记库</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem>
<Plus size={16} className="mr-2" />
空白笔记
</DropdownMenuItem>
<DropdownMenuItem>
<Plus size={16} className="mr-2" />
从模板创建
</DropdownMenuItem>
<DropdownMenuItem>
<Plus size={16} className="mr-2" />
设置待办项
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Plus size={16} className="mr-2" />
新的笔记库
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}
}
Loading