feat: add CLAUDE.md for development guidance and update .gitignore to include .claude directory; refactor AIChatPanel and AppShell components for improved context handling; simplify layout in ProjectDetail, NoteDetailPage, TasksPage, and TimelinePage components
This commit is contained in:
@@ -4,7 +4,6 @@ import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import { useAIChat, type ChatContext } from '@/hooks/useAIChat';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -19,19 +18,11 @@ const SUGGESTION_CHIPS = [
|
||||
|
||||
interface AIChatPanelProps {
|
||||
onOpenSettings?: () => void;
|
||||
contextType: 'global' | 'project';
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
curtainOpen: boolean;
|
||||
isHomePage?: boolean;
|
||||
}
|
||||
|
||||
export function AIChatPanel({
|
||||
onOpenSettings,
|
||||
contextType,
|
||||
projectId,
|
||||
projectName,
|
||||
curtainOpen,
|
||||
isHomePage,
|
||||
}: AIChatPanelProps) {
|
||||
const hasTokenQuery = trpc.ai.hasToken.useQuery();
|
||||
@@ -41,11 +32,8 @@ export function AIChatPanel({
|
||||
const dueTodayQuery = trpc.tasks.dueToday.useQuery(undefined, { enabled: !!isHomePage });
|
||||
|
||||
const chatContext = useMemo<ChatContext>(
|
||||
() => ({
|
||||
type: contextType,
|
||||
...(contextType === 'project' && projectId ? { projectId } : {}),
|
||||
}),
|
||||
[contextType, projectId],
|
||||
() => ({ type: 'global' as const }),
|
||||
[],
|
||||
);
|
||||
const {
|
||||
messages,
|
||||
@@ -71,15 +59,6 @@ export function AIChatPanel({
|
||||
if (el) el.scrollTo({ top: el.scrollHeight });
|
||||
}, []);
|
||||
|
||||
// Reset input when curtain closes; scroll to bottom when it reopens
|
||||
useEffect(() => {
|
||||
if (!curtainOpen) {
|
||||
setInput('');
|
||||
} else {
|
||||
setTimeout(scrollToBottom, 50);
|
||||
}
|
||||
}, [curtainOpen, scrollToBottom]);
|
||||
|
||||
// Auto-scroll when messages change or streaming content updates
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
@@ -130,60 +109,15 @@ export function AIChatPanel({
|
||||
}
|
||||
};
|
||||
|
||||
// Smart wheel handler: only stop propagation when there's content to scroll through
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
const el = messagesContainerRef.current;
|
||||
if (!el) return;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 2;
|
||||
const atTop = el.scrollTop < 2;
|
||||
// Let event propagate to AppShell when at boundaries
|
||||
if ((e.deltaY > 0 && atBottom) || (e.deltaY < 0 && atTop)) return;
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// No token configured — show settings prompt
|
||||
if (hasTokenQuery.data === false && !isHomePage) {
|
||||
return (
|
||||
<div className="absolute inset-0 z-0 flex flex-col items-center justify-center bg-background">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent className="flex flex-col items-center gap-4 pt-6">
|
||||
<KeyRound size={32} className="text-muted-foreground" />
|
||||
<div className="text-center space-y-1">
|
||||
<p className="text-sm font-medium">AI provider not configured</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect your GitHub Copilot token to enable AI-powered features
|
||||
like chat, summaries, and suggestions.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={onOpenSettings}>
|
||||
Open Settings
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasMessages = messages.length > 0 || isStreaming;
|
||||
|
||||
const contextLabel =
|
||||
contextType === 'project' && projectName
|
||||
? `Chatting about: ${projectName}`
|
||||
: 'Global workspace';
|
||||
|
||||
// Derived values for home page
|
||||
const dueCount = dueTodayQuery.data?.length ?? 0;
|
||||
const userName = userNameQuery.data ?? 'there';
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-0 flex flex-col bg-background">
|
||||
{/* Context header (non-home) */}
|
||||
{!isHomePage && (
|
||||
<div className="flex items-center gap-2 px-6 pt-4 pb-2">
|
||||
<Badge variant="outline">{contextLabel}</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scrollable messages area */}
|
||||
<ScrollArea
|
||||
className="flex-1 min-h-0"
|
||||
@@ -193,7 +127,6 @@ export function AIChatPanel({
|
||||
? '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-center'
|
||||
: '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-end'
|
||||
}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
{/* Home page initial state: greeting + brief */}
|
||||
{isHomePage && !hasMessages && (
|
||||
@@ -246,7 +179,6 @@ export function AIChatPanel({
|
||||
onInputChange={setInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onSend={handleSend}
|
||||
isHomePage={isHomePage}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 mt-4">
|
||||
{SUGGESTION_CHIPS.map((chip) => (
|
||||
@@ -336,79 +268,19 @@ export function AIChatPanel({
|
||||
)}
|
||||
|
||||
{/* Non-home messages */}
|
||||
{!isHomePage && hasMessages && (
|
||||
<div className="mx-auto w-full max-w-[1088px] px-6 pt-4 pb-32">
|
||||
<div className="flex flex-col gap-4">
|
||||
{messages.map((msg) => {
|
||||
if (msg.role === 'user') {
|
||||
return (
|
||||
<div key={msg.id} className="flex justify-end">
|
||||
<div className="ml-auto max-w-[75%] rounded-2xl bg-muted px-4 py-2">
|
||||
<ChatMarkdown content={msg.content} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.error) {
|
||||
return (
|
||||
<div key={msg.id} className="mr-auto max-w-[75%]">
|
||||
<p className="text-sm text-destructive whitespace-pre-wrap">
|
||||
{msg.content}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={msg.id} className="mr-auto max-w-[75%]">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Sparkles size={16} className="text-foreground" />
|
||||
<span className="text-sm font-semibold">Adiuva</span>
|
||||
</div>
|
||||
<div className="pl-[22px]">
|
||||
<ChatMarkdown content={msg.content} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Streaming AI response */}
|
||||
{isStreaming && (
|
||||
<div className="mr-auto max-w-[75%]">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Sparkles size={16} className="text-foreground" />
|
||||
<span className="text-sm font-semibold">Adiuva</span>
|
||||
</div>
|
||||
{streamingContent ? (
|
||||
<div className="pl-[22px]">
|
||||
<ChatMarkdown content={streamingContent} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 pl-[22px]">
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
{/* Fixed input — pinned to the bottom (hidden on home initial state) */}
|
||||
{!(isHomePage && !hasMessages) && (
|
||||
{/* Fixed input — pinned to the bottom (hidden on initial state) */}
|
||||
{hasMessages && (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-6 pb-5 pt-16 pointer-events-none">
|
||||
<div className="absolute inset-x-0 top-0 h-full bg-gradient-to-b from-transparent via-background/60 to-background/90" />
|
||||
<div className={`relative pointer-events-auto mx-auto ${isHomePage ? 'max-w-3xl' : 'max-w-[1088px]'}`}>
|
||||
<div className="relative pointer-events-auto mx-auto max-w-3xl">
|
||||
<ChatInput
|
||||
input={input}
|
||||
isStreaming={isStreaming || briefLoading}
|
||||
onInputChange={setInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onSend={handleSend}
|
||||
isHomePage={isHomePage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,7 +297,6 @@ interface ChatInputProps {
|
||||
onInputChange: (value: string) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onSend: () => void;
|
||||
isHomePage?: boolean;
|
||||
}
|
||||
|
||||
function ChatInput({
|
||||
|
||||
Reference in New Issue
Block a user