diff --git a/app/(app)/chat/page.tsx b/app/(app)/chat/page.tsx new file mode 100644 index 0000000..e9035db --- /dev/null +++ b/app/(app)/chat/page.tsx @@ -0,0 +1,18 @@ +import { redirect } from "next/navigation"; + +import { auth } from "@/auth"; +import { getAiSettingsView } from "@/lib/ai-settings"; +import { ChatPageClient } from "@/components/chat/chat-page-client"; + +export default async function ChatPage() { + const session = await auth(); + if (!session?.user) redirect("/login"); + + const aiSettings = await getAiSettingsView(); + const aiConfigured = !!(aiSettings.apiUrl && aiSettings.model); + + // No board data fetched here -- sendChatMessage (lib/actions/chat-ai.ts) + // gathers and re-serializes the user's full context itself on every + // turn, so this page only needs the aiConfigured gate. + return ; +} diff --git a/components/ai/chat-thread.tsx b/components/ai/chat-thread.tsx new file mode 100644 index 0000000..2b4bf94 --- /dev/null +++ b/components/ai/chat-thread.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Mic, Send, Square } from "lucide-react"; +import { MarkdownPreview } from "@/components/markdown/markdown-widgets"; + +export interface ChatBubbleMessage { + role: "user" | "assistant"; + content: string; +} + +/** + * Shared message-bubble list for the two plain-text chat surfaces (the + * global Chat page and the group Status Update follow-up Q&A) -- both + * just render a back-and-forth conversation with no ask/finalize review + * step, unlike GroupAiDialog/TodoAiDialog (which inline their own nearly + * identical list because they also render an editable "pending" state + * this shared component doesn't need to know about). + */ +export function ChatMessageList({ + messages, + sending, + error, + emptyHint, + colorMode, +}: { + messages: ChatBubbleMessage[]; + sending: boolean; + error: string | null; + emptyHint: string; + colorMode?: "light" | "dark"; +}) { + return ( +
+ {messages.length === 0 ? ( +

+ {emptyHint} +

+ ) : ( + messages.map((message, index) => ( +
+ {message.role === "assistant" ? ( + + ) : ( + message.content + )} +
+ )) + )} + {sending && ( +
+ Thinking… +
+ )} + {error &&

{error}

} +
+ ); +} + +/** + * Shared textarea + mic + send composer, matching the input row already + * used by GroupAiDialog/TodoAiDialog (voice input via + * useSpeechRecognition, Enter-to-send, Shift+Enter for a newline). + */ +export function ChatComposer({ + value, + onChange, + onSend, + sending, + listening, + micSupported, + onStartListening, + onStopListening, + placeholder, +}: { + value: string; + onChange: (value: string) => void; + onSend: () => void; + sending: boolean; + listening: boolean; + micSupported: boolean; + onStartListening: () => void; + onStopListening: () => void; + placeholder?: string; +}) { + return ( +
+