diff --git a/.github/workflows/scour.yml b/.github/workflows/scour.yml new file mode 100644 index 0000000..ab5d0f8 --- /dev/null +++ b/.github/workflows/scour.yml @@ -0,0 +1,23 @@ +name: Scour + +on: + pull_request: + types: [opened, synchronize] + +jobs: + scour: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run scour + uses: carsonSgit/scour@v0.4.0 + with: + fail-on: error + triage: "true" diff --git a/.gitignore b/.gitignore index 55f0e7c..31d72ef 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,9 @@ next-env.d.ts /videos/*.mov /videos/*.avi /videos/*.mkv +.codex +.claude +.agents +.mcp.json +.omx +.cursor diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..82b8a5e --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=@mux/mux-player diff --git a/app/(dashboard)/ai-chat/[id]/chat-client.tsx b/app/(dashboard)/ai-chat/[id]/chat-client.tsx index 27fc83d..e4e69e3 100644 --- a/app/(dashboard)/ai-chat/[id]/chat-client.tsx +++ b/app/(dashboard)/ai-chat/[id]/chat-client.tsx @@ -3,13 +3,16 @@ import type { UIMessage } from "@ai-sdk/react"; import { useChat } from "@ai-sdk/react"; import { + IconArrowUp, IconBolt, IconDatabase, - IconSparkles, + IconFeather, IconUniverse, } from "@tabler/icons-react"; -import { createIdGenerator, DefaultChatTransport } from "ai"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { createIdGenerator, DefaultChatTransport, type ToolUIPart } from "ai"; +import { motion } from "framer-motion"; +import type { ReactNode } from "react"; +import { useMemo, useRef, useState } from "react"; import { AssetDisplay } from "@/components/ai-elements/asset"; import { Conversation, @@ -19,15 +22,11 @@ import { EventCard } from "@/components/ai-elements/event-card"; import { Loader } from "@/components/ai-elements/loader"; import { Message, MessageContent } from "@/components/ai-elements/message"; import { - PromptInput, - PromptInputFooter, PromptInputModelSelect, PromptInputModelSelectContent, PromptInputModelSelectItem, PromptInputModelSelectTrigger, PromptInputModelSelectValue, - PromptInputSubmit, - PromptInputTextarea, PromptInputTools, } from "@/components/ai-elements/prompt-input"; import { @@ -72,18 +71,64 @@ interface ChatClientProps { initialMessages: UIMessage[]; } +/** Shape of a tool-invocation message part as streamed by the AI SDK. */ +interface ToolMessagePart { + state?: ToolUIPart["state"]; + output?: ToolUIPart["output"]; + errorText?: ToolUIPart["errorText"]; + toolName?: string; + text?: string; +} + +/** + * Renders a tool-invocation part's lifecycle: a loading line while running, + * the output via `renderOutput` when available, and an error line on failure. + */ +function ToolStatePart({ + part, + loadingLabel, + errorLabel, + renderOutput, +}: { + part: ToolMessagePart; + loadingLabel: string; + errorLabel: string; + renderOutput: (output: any) => ReactNode; +}) { + if (part.state === "input-available") { + return ( +
+
{loadingLabel}
+
+ ); + } + if (part.state === "output-available") { + return
{renderOutput(part.output)}
; + } + if (part.state === "output-error") { + return ( +
+ {errorLabel}: {part.errorText} +
+ ); + } + return null; +} + +const STARTER_PROMPTS = [ + "Show me the highest-severity events", + "What did the cameras detect today?", + "Summarize activity at the loading dock", + "Create an incident report for this week", +]; + export default function ChatClient({ id, initialMessages }: ChatClientProps) { const [selectedModel, setSelectedModel] = useState("claude-haiku-4.5"); const [input, setInput] = useState(""); const selectedModelRef = useRef(selectedModel); + selectedModelRef.current = selectedModel; - // Keep ref in sync with state - useEffect(() => { - selectedModelRef.current = selectedModel; - }, [selectedModel]); - - // Create a custom transport that reads model at request time - // Using useMemo to ensure transport is only created once per chat id + // Created once per chat id; reads the selected model at request time via ref. const transport = useMemo( () => new DefaultChatTransport({ @@ -96,15 +141,6 @@ export default function ChatClient({ id, initialMessages }: ChatClientProps) { [id], ); - // Cleanup transport on unmount - useEffect(() => { - return () => { - // DefaultChatTransport doesn't have explicit cleanup methods, - // but we ensure it's garbage collected by clearing the reference - // The abort controller in the transport will handle ongoing requests - }; - }, [transport]); - const { messages, sendMessage, status } = useChat({ id, messages: initialMessages, @@ -114,27 +150,137 @@ export default function ChatClient({ id, initialMessages }: ChatClientProps) { }), transport, }); + const ready = status === "ready"; + + const submitPrompt = (text: string) => { + const trimmed = text.trim(); + if (!trimmed || !ready) return; + sendMessage({ text: trimmed }); + setInput(""); + }; + + const modelPicker = ( + + + + + + + + + + + + Claude Sonnet 4.5 + + + + Advanced reasoning, complex analysis, and deep thinking + capabilities + + + + + + + + Claude Haiku 4.5 + + + + Fast, efficient responses with excellent accuracy + + + + + + + + Kimi K2 (Provided by Groq) + + + + High-performance alternative with rapid response times + + + + + + + + Stateful (Letta Agent) + + + + Long-term memory, file system access, and self-improvement + capabilities + + + + + + + ); return ( -
+
-
- - +
+ + {messages.length === 0 && ( -
-
-
-

- Start a conversation -

-

- Ask me anything about your surveillance footage +

+
+ +
+

+ Argus AI

+

+ Ask about your footage. +

+

+ Search events, open recordings, summarize activity, and + generate investigation reports from the demo workspace. +

+
+ + + +
+ {STARTER_PROMPTS.map((prompt) => ( + submitPrompt(prompt)} + disabled={!ready} + whileHover={{ y: -2 }} + whileTap={{ scale: 0.99 }} + transition={{ duration: 0.16, ease: "easeOut" }} + className="cursor-pointer rounded-lg bg-card px-3.5 py-3 text-left text-muted-foreground text-sm leading-5 shadow-sm ring-1 ring-black/5 transition-colors hover:bg-accent hover:text-foreground disabled:opacity-50 dark:ring-white/10" + > + {prompt} + + ))}
-
+
)} @@ -148,12 +294,10 @@ export default function ChatClient({ id, initialMessages }: ChatClientProps) { {message.parts.map((part, index) => { - // Render text parts if (part.type === "text") { return {part.text}; } - // Render reasoning parts if (part.type === "reasoning") { return ( - {(part as any).text} + {(part as ToolMessagePart).text ?? ""} ); } - // Render event display tools with EventCard if ( part.type === "tool-displayEvent" || part.type === "tool-displayEventById" ) { - const state = (part as any).state; - - if (state === "input-available") { - return ( -
-
- Loading event... -
-
- ); - } - - if (state === "output-available") { - return ( -
- -
- ); - } - - if (state === "output-error") { - return ( -
- Error loading event: {(part as any).errorText} -
- ); - } - - return null; + return ( + } + /> + ); } - // Render asset display tools with AssetDisplay if (part.type === "tool-displayAsset") { - const state = (part as any).state; - - if (state === "input-available") { - return ( -
-
- Loading video... -
-
- ); - } - - if (state === "output-available") { - return ( -
- -
- ); - } - - if (state === "output-error") { - return ( -
- Error loading video: {(part as any).errorText} -
- ); - } - - return null; + return ( + ( + + )} + /> + ); } - // Render report creation tool with Report preview if (part.type === "tool-createReport") { - const state = (part as any).state; - - if (state === "input-available") { - return ( -
-
- Creating report... -
-
- ); - } - - if (state === "output-available") { - return ( -
- -
- ); - } - - if (state === "output-error") { - return ( -
- Error creating report: {(part as any).errorText} -
- ); - } - - return null; + return ( + } + /> + ); } - // Render tool invocations (both static and dynamic) + // Generic fallback for any other tool invocation. if ( part.type.startsWith("tool-") || part.type === "dynamic-tool" ) { + const p = part as ToolMessagePart; const toolName = part.type === "dynamic-tool" - ? (part as any).toolName + ? p.toolName : part.type.replace("tool-", ""); - const displayName = TOOL_NAME_MAP[toolName] || toolName; + const displayName = + TOOL_NAME_MAP[toolName ?? ""] || toolName; const toolType = part.type === "dynamic-tool" - ? (`tool-${(part as any).toolName}` as `tool-${string}`) + ? (`tool-${p.toolName}` as `tool-${string}`) : (part.type as `tool-${string}`); return ( - {(part as any).state === "output-available" && ( + {p.state === "output-available" && ( )} @@ -342,94 +409,92 @@ export default function ChatClient({ id, initialMessages }: ChatClientProps) { -
-
- { - event.preventDefault(); - if (input.trim() && status === "ready") { - sendMessage({ text: input }); - setInput(""); - } - }} - > - setInput(e.target.value)} - placeholder="Ask about your videos..." - value={input} - disabled={status !== "ready"} + {messages.length > 0 && ( +
+
+ - - - - - - - - - - - - - Claude Sonnet 4.5 - - - - Advanced reasoning, complex analysis, and deep - thinking capabilities - - - - - - - - Claude Haiku 4.5 - - - - Fast, efficient responses with excellent accuracy - - - - - - - - Kimi K2 (Provided by Groq) - - - - High-performance alternative with rapid response - times - - - - - - - - Stateful (Letta Agent) - - - - Long-term memory, file system access, and - self-improvement capabilities - - - - - - - - - +
-
+ )}
); } + +function ChatHeroComposer({ + input, + ready, + helper, + modelPicker, + compact = false, + onInputChange, + onSubmit, +}: { + input: string; + ready: boolean; + helper: string; + modelPicker: ReactNode; + compact?: boolean; + onInputChange: (value: string) => void; + onSubmit: (text: string) => void; +}) { + const submit = () => { + if (!ready) return; + onSubmit(input); + }; + + return ( + +
+