tr]:last:border-b-0",
48 | className
49 | )}
50 | {...props}
51 | />
52 | )
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | )
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className
75 | )}
76 | {...props}
77 | />
78 | )
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | | [role=checkbox]]:translate-y-[2px]",
87 | className
88 | )}
89 | {...props}
90 | />
91 | )
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | )
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | }
117 |
--------------------------------------------------------------------------------
/src/components/TooltipUsage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState, useEffect } from "react";
2 | import {
3 | Tooltip,
4 | TooltipTrigger,
5 | TooltipContent,
6 | } from "@/components/ui/tooltip";
7 | import { intervalToDuration, differenceInSeconds } from "date-fns";
8 | import { useUserLimits } from "@/hooks/UserLimitsContext";
9 |
10 | function formatTimeRemaining(resetTimestamp: number) {
11 | const now = new Date();
12 | const reset =
13 | typeof resetTimestamp === "string"
14 | ? new Date(parseInt(resetTimestamp, 10))
15 | : new Date(resetTimestamp);
16 | if (isNaN(reset.getTime())) return "--:--:--";
17 | // Only show if in the future
18 | if (reset.getTime() <= now.getTime()) return "00:00:00";
19 | // Use date-fns to get the duration breakdown
20 | const duration = intervalToDuration({ start: now, end: reset });
21 | // Calculate total hours (including days, months, years)
22 | const totalSeconds = differenceInSeconds(reset, now);
23 | const hours = Math.floor(totalSeconds / 3600)
24 | .toString()
25 | .padStart(2, "0");
26 | const minutes = (duration.minutes ?? 0).toString().padStart(2, "0");
27 | const seconds = (duration.seconds ?? 0).toString().padStart(2, "0");
28 | return `${hours}:${minutes}:${seconds}`;
29 | }
30 |
31 | export default function TooltipUsage() {
32 | const { remainingMessages, resetTimestamp, loading } = useUserLimits();
33 |
34 | const [open, setOpen] = useState(false);
35 | const [tick, setTick] = useState(0);
36 |
37 | useEffect(() => {
38 | if (!open) return;
39 | const interval = setInterval(() => setTick((t) => t + 1), 1000);
40 | return () => clearInterval(interval);
41 | }, [open]);
42 |
43 | const formattedTime = useMemo(() => {
44 | if (!resetTimestamp) return undefined;
45 | return formatTimeRemaining(resetTimestamp);
46 | // Include tick so it recalculates every second when open
47 | }, [resetTimestamp, tick]);
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 | 
56 |
57 | {remainingMessages}
58 |
59 |
60 | credits
61 |
62 |
63 |
64 | {formattedTime && (
65 |
66 |
67 |
68 | Time remaining until refill:
69 |
70 | {formattedTime}
71 |
72 |
73 | )}
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import Link from "next/link";
5 | import React, { useEffect, useState } from "react";
6 |
7 | import { ChatHistoryMenu } from "./ChatHistoryMenu";
8 | import { GithubBanner } from "./GithubBanner";
9 | import useLocalStorage from "@/hooks/useLocalStorage";
10 | import { cn } from "@/lib/utils";
11 | import TooltipUsage from "./TooltipUsage";
12 |
13 | interface HeaderProps {
14 | chatId?: string;
15 | }
16 |
17 | export function Header({ chatId }: HeaderProps) {
18 | const [showBanner, setShowBanner] = useLocalStorage(
19 | "showBanner",
20 | true
21 | );
22 | const [mounted, setMounted] = useState(false);
23 |
24 | useEffect(() => {
25 | setMounted(true);
26 | }, []);
27 |
28 | if (!mounted) return <>>;
29 |
30 | return (
31 | <>
32 |
86 | {/* Spacer for mobile header height */}
87 | setShowBanner(false)} />
88 |
94 |
100 | >
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/chat/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Header } from "@/components/header";
4 | import { cn } from "@/lib/utils";
5 |
6 | export default function Loading() {
7 | return (
8 |
9 |
10 |
11 |
12 | {/* User bubble 1 (right) */}
13 |
21 | {/* Assistant bubble 1 (left) */}
22 |
30 | {/* User bubble 2 (right) */}
31 |
39 | {/* Assistant bubble 2 (left) */}
40 |
48 | {/* Assistant bubble 3 */}
49 |
57 | {/* Assistant bubble 4 */}
58 |
66 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/public/suggestion.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/public/githubLogo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @theme inline {
5 | --color-background: var(--background);
6 | --color-foreground: var(--foreground);
7 | --font-sans: var(--font-instrument-sans);
8 | --font-mono: var(--font-geist-mono);
9 | --color-sidebar-ring: var(--sidebar-ring);
10 | --color-sidebar-border: var(--sidebar-border);
11 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
12 | --color-sidebar-accent: var(--sidebar-accent);
13 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
14 | --color-sidebar-primary: var(--sidebar-primary);
15 | --color-sidebar-foreground: var(--sidebar-foreground);
16 | --color-sidebar: var(--sidebar);
17 | --color-chart-5: var(--chart-5);
18 | --color-chart-4: var(--chart-4);
19 | --color-chart-3: var(--chart-3);
20 | --color-chart-2: var(--chart-2);
21 | --color-chart-1: var(--chart-1);
22 | --color-ring: var(--ring);
23 | --color-input: var(--input);
24 | --color-border: var(--border);
25 | --color-destructive: var(--destructive);
26 | --color-accent-foreground: var(--accent-foreground);
27 | --color-accent: var(--accent);
28 | --color-muted-foreground: var(--muted-foreground);
29 | --color-muted: var(--muted);
30 | --color-secondary-foreground: var(--secondary-foreground);
31 | --color-secondary: var(--secondary);
32 | --color-primary-foreground: var(--primary-foreground);
33 | --color-primary: var(--primary);
34 | --color-popover-foreground: var(--popover-foreground);
35 | --color-popover: var(--popover);
36 | --color-card-foreground: var(--card-foreground);
37 | --color-card: var(--card);
38 | --radius-sm: calc(var(--radius) - 4px);
39 | --radius-md: calc(var(--radius) - 2px);
40 | --radius-lg: var(--radius);
41 | --radius-xl: calc(var(--radius) + 4px);
42 | }
43 |
44 | :root {
45 | --radius: 0.625rem;
46 | --background: oklch(1 0 0);
47 | --foreground: oklch(0.145 0 0);
48 | --card: oklch(1 0 0);
49 | --card-foreground: oklch(0.145 0 0);
50 | --popover: oklch(1 0 0);
51 | --popover-foreground: oklch(0.145 0 0);
52 | --primary: oklch(0.205 0 0);
53 | --primary-foreground: oklch(0.985 0 0);
54 | --secondary: oklch(0.97 0 0);
55 | --secondary-foreground: oklch(0.205 0 0);
56 | --muted: oklch(0.97 0 0);
57 | --muted-foreground: oklch(0.556 0 0);
58 | --accent: oklch(0.97 0 0);
59 | --accent-foreground: oklch(0.205 0 0);
60 | --destructive: oklch(0.577 0.245 27.325);
61 | --border: oklch(0.922 0 0);
62 | --input: oklch(0.922 0 0);
63 | --ring: oklch(0.708 0 0);
64 | --chart-1: oklch(0.646 0.222 41.116);
65 | --chart-2: oklch(0.6 0.118 184.704);
66 | --chart-3: oklch(0.398 0.07 227.392);
67 | --chart-4: oklch(0.828 0.189 84.429);
68 | --chart-5: oklch(0.769 0.188 70.08);
69 | --sidebar: oklch(0.985 0 0);
70 | --sidebar-foreground: oklch(0.145 0 0);
71 | --sidebar-primary: oklch(0.205 0 0);
72 | --sidebar-primary-foreground: oklch(0.985 0 0);
73 | --sidebar-accent: oklch(0.97 0 0);
74 | --sidebar-accent-foreground: oklch(0.205 0 0);
75 | --sidebar-border: oklch(0.922 0 0);
76 | --sidebar-ring: oklch(0.708 0 0);
77 | }
78 |
79 | @layer base {
80 | * {
81 | @apply border-border outline-ring/50;
82 | }
83 | body {
84 | @apply bg-background text-foreground;
85 | }
86 | }
87 |
88 | code, pre, .code {
89 | font-size: 12px !important;
90 | }
91 |
92 | /* Custom light prose styles for lists */
93 | .prose ol,
94 | .prose ul {
95 | padding-left: 1.5rem;
96 | margin-top: 0.5em;
97 | margin-bottom: 0.5em;
98 | }
99 | .prose ol {
100 | list-style-type: decimal;
101 | }
102 | .prose ul {
103 | list-style-type: disc;
104 | }
105 | .prose li {
106 | margin-top: 0.25em;
107 | margin-bottom: 0.25em;
108 | padding-left: 0.25em;
109 | }
110 | .prose ol > li::marker {
111 | color: var(--primary);
112 | font-weight: bold;
113 | }
114 | .prose ul > li::marker {
115 | color: var(--primary);
116 | }
117 | .dark .prose ol > li::marker,
118 | .dark .prose ul > li::marker {
119 | color: var(--primary-foreground);
120 | }
121 |
--------------------------------------------------------------------------------
/src/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { togetherAISDKClient } from "@/lib/clients";
2 | import {
3 | streamText,
4 | generateId,
5 | CoreMessage,
6 | appendResponseMessages,
7 | wrapLanguageModel,
8 | extractReasoningMiddleware,
9 | } from "ai";
10 | import { DbMessage, loadChat, saveNewMessage } from "@/lib/chat-store";
11 | import { limitMessages } from "@/lib/limits";
12 | import { generateCodePrompt } from "@/lib/prompts";
13 | import { CHAT_MODELS } from "@/lib/models";
14 |
15 | export async function POST(req: Request) {
16 | const { id, message, model } = await req.json();
17 |
18 | // get from headers X-Auto-Error-Resolved
19 | const errorResolved = req.headers.get("X-Auto-Error-Resolved");
20 |
21 | // Use IP address as a simple user fingerprint
22 | const ip = req.headers.get("x-forwarded-for") || "unknown";
23 | try {
24 | if (!errorResolved) {
25 | await limitMessages(ip);
26 | }
27 | } catch (err) {
28 | return new Response("Too many messages. Daily limit reached.", {
29 | status: 429,
30 | });
31 | }
32 |
33 | const chat = await loadChat(id);
34 |
35 | const newUserMessage: DbMessage = {
36 | id: generateId(),
37 | role: "user",
38 | content: message,
39 | createdAt: new Date(),
40 | isAutoErrorResolution: errorResolved === "true",
41 | };
42 |
43 | // Save the new user message
44 | await saveNewMessage({ id, message: newUserMessage });
45 |
46 | const messagesToSave: DbMessage[] = [
47 | ...(chat?.messages || []),
48 | newUserMessage,
49 | ];
50 |
51 | const coreMessagesForStream = messagesToSave
52 | .filter((msg) => msg.role === "user" || msg.role === "assistant")
53 | .map((msg) => ({
54 | role: msg.role,
55 | content: msg.content,
56 | }));
57 |
58 | // Start timing
59 | const start = Date.now();
60 |
61 | // Determine which model to use
62 |
63 | const defaultModel = CHAT_MODELS.find((m) => m.isDefault)?.model;
64 |
65 | const selectedModelSlug = typeof model === "string" ? model : undefined;
66 |
67 | const selectedModel =
68 | (selectedModelSlug &&
69 | CHAT_MODELS.find((m) => m.slug === selectedModelSlug)?.model) ||
70 | defaultModel;
71 |
72 | if (!selectedModel) {
73 | throw new Error("Invalid model selected.");
74 | }
75 |
76 | try {
77 | // Create a new model instance based on selectedModel
78 | const modelInstance = wrapLanguageModel({
79 | model: togetherAISDKClient(selectedModel),
80 | middleware: extractReasoningMiddleware({ tagName: "think" }),
81 | });
82 |
83 | // TODO: handling context length here cause coreMessagesForStream could be too long for the currently selected model?
84 |
85 | const stream = streamText({
86 | model: modelInstance,
87 | system: generateCodePrompt({
88 | csvFileUrl: chat?.csvFileUrl || "",
89 | csvHeaders: chat?.csvHeaders || [],
90 | csvRows: chat?.csvRows || [],
91 | }),
92 | messages: coreMessagesForStream.filter(
93 | (msg) => msg.role !== "system"
94 | ) as CoreMessage[],
95 | onError: (error) => {
96 | console.error("Error:", error);
97 | },
98 | async onFinish({ response }) {
99 | // End timing
100 | const end = Date.now();
101 | const duration = (end - start) / 1000;
102 |
103 | if (response.messages.length > 1) {
104 | console.log("response.messages", response.messages);
105 | return;
106 | }
107 |
108 | const responseMessages = appendResponseMessages({
109 | messages: messagesToSave,
110 | responseMessages: response.messages,
111 | });
112 |
113 | const responseMessage = responseMessages.at(-1);
114 |
115 | if (!responseMessage) {
116 | return;
117 | }
118 |
119 | await saveNewMessage({
120 | id,
121 | message: {
122 | ...responseMessage,
123 | duration,
124 | model: selectedModel,
125 | },
126 | });
127 | },
128 | });
129 |
130 | return stream.toDataStreamResponse({
131 | sendReasoning: true,
132 | });
133 | } catch (err) {
134 | console.error(err);
135 | return new Response("Error generating response", { status: 500 });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/components/PromptInput.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type React from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { ModelDropdown } from "./ModelDropdown";
6 | import { useLLMModel } from "@/hooks/useLLMModel";
7 | import { useEffect, useRef } from "react";
8 | import { cn, UploadedFile } from "@/lib/utils";
9 | import { DropdownFileActions } from "./DropdownFileActions";
10 |
11 | export function PromptInput({
12 | isLLMAnswering,
13 | onStopLLM,
14 | textAreaClassName,
15 | value,
16 | onChange,
17 | onSend,
18 | uploadedFile,
19 | placeholder = "Ask anything...",
20 | }: {
21 | isLLMAnswering: boolean;
22 | onStopLLM: () => void;
23 | textAreaClassName?: string;
24 | value: string;
25 | onChange: (value: string) => void;
26 | onSend: () => void;
27 | uploadedFile?: UploadedFile;
28 | placeholder?: string;
29 | }) {
30 | const { selectedModelSlug, setModel, models } = useLLMModel();
31 | // Autofocus logic
32 | const textareaRef = useRef(null);
33 | useEffect(() => {
34 | textareaRef.current?.focus();
35 | }, []);
36 |
37 | const handleKeyDown = (e: React.KeyboardEvent) => {
38 | if (e.key === "Enter" && !e.shiftKey) {
39 | e.preventDefault();
40 | onSend();
41 | }
42 | };
43 |
44 | const handlePaste = (e: React.ClipboardEvent) => {
45 | const pastedText = e.clipboardData.getData("text");
46 | onChange(pastedText.trim());
47 | e.preventDefault(); // Prevent default paste behavior
48 | };
49 |
50 | return (
51 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/upload-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Dropzone from "react-dropzone";
3 | import React from "react";
4 | import { toast } from "sonner";
5 | import { cn, EXAMPLE_FILE_URL } from "@/lib/utils";
6 |
7 | interface UploadAreaProps {
8 | onFileChange: (file: File | null) => void;
9 | uploadedFile: File | null;
10 | }
11 |
12 | export function UploadArea({ onFileChange, uploadedFile }: UploadAreaProps) {
13 | if (uploadedFile) return <>>;
14 |
15 | const onUseExample = async () => {
16 | try {
17 | const response = await fetch(EXAMPLE_FILE_URL);
18 | const blob = await response.blob();
19 | const file = new File([blob], "products.csv", {
20 | type: "text/csv",
21 | });
22 | onFileChange(file);
23 | } catch (error) {
24 | toast.error("Failed to load example CSV");
25 | }
26 | };
27 |
28 | return (
29 | <>
30 |
31 | {
38 | const file = acceptedFiles[0];
39 |
40 | if (!file) {
41 | toast.warning("Please upload a CSV file");
42 | return;
43 | }
44 |
45 | if (file.size > 30 * 1024 * 1024) {
46 | toast.warning("File size must be less than 30MB");
47 | return;
48 | }
49 | onFileChange(file);
50 | }}
51 | >
52 | {({ getRootProps, getInputProps, isDragAccept }) => (
53 |
57 |
58 |
59 |
70 |
71 | Upload your CSV first, then ask a question
72 |
73 |
74 |
75 |
76 | or drag and drop here
77 |
78 |
84 |
85 | 
91 |
92 | Upload CSV
93 |
94 |
95 |
96 |
97 |
98 |
99 | )}
100 |
101 |
102 |
110 | >
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/lib/prompts.ts:
--------------------------------------------------------------------------------
1 | export const generateCodePrompt = ({
2 | csvFileUrl,
3 | csvHeaders,
4 | csvRows,
5 | }: {
6 | csvFileUrl?: string;
7 | csvHeaders?: string[];
8 | csvRows?: { [key: string]: string }[];
9 | }) => {
10 | // Prepare sample rows as a markdown table if available
11 | let sampleRowsSection = "";
12 | if (csvRows && csvRows.length > 0 && csvHeaders && csvHeaders.length > 0) {
13 | const sampleRows = csvRows.slice(0, 3);
14 | const headerRow = `| ${csvHeaders.join(" | ")} |`;
15 | const separatorRow = `|${csvHeaders.map(() => "---").join("|")}|`;
16 | const dataRows = sampleRows
17 | .map((row) => `| ${csvHeaders.map((h) => row[h] ?? "").join(" | ")} |`)
18 | .join("\n");
19 | sampleRowsSection = `\n\nHere are a few sample rows from the dataset:\n\n${headerRow}\n${separatorRow}\n${dataRows}`;
20 | }
21 |
22 | return `
23 | You are an expert data scientist assistant that writes python code to answer questions about a dataset.
24 |
25 | You are given a dataset and a question.
26 |
27 | The dataset is available at the following S3 URL: ${
28 | csvFileUrl || "[NO FILE URL PROVIDED]"
29 | }
30 | The dataset has the following columns: ${
31 | csvHeaders?.join(", ") || "[NO HEADERS PROVIDED]"
32 | }
33 | ${sampleRowsSection}
34 |
35 | You must always write python code that:
36 | - Downloads the CSV from the provided S3 URL (using requests or pandas.read_csv).
37 | - Uses the provided columns for analysis.
38 | - Never outputs more than one graph per code response. If a question could be answered with multiple graphs, choose the most relevant or informative one and only output that single graph. This is to prevent slow output.
39 | - When generating a graph, always consider how many values (bars, colors, lines, etc.) can be clearly displayed. Do not attempt to show thousands of values in a single graph; instead, limit the number of displayed values to a reasonable amount (e.g., 10-20) so the graph remains readable and informative. If there are too many categories or data points, select the most relevant or aggregate them appropriately.
40 | - Never generate HTML output. Only use Python print statements or graphs/plots for output.
41 |
42 | Always return the python code in a single unique code block.
43 |
44 | Python sessions come pre-installed with the following dependencies, any other dependencies can be installed using a !pip install command in the python code.
45 |
46 | - aiohttp
47 | - beautifulsoup4
48 | - bokeh
49 | - gensim
50 | - imageio
51 | - joblib
52 | - librosa
53 | - matplotlib
54 | - nltk
55 | - numpy
56 | - opencv-python
57 | - openpyxl
58 | - pandas
59 | - plotly
60 | - pytest
61 | - python-docx
62 | - pytz
63 | - requests
64 | - scikit-image
65 | - scikit-learn
66 | - scipy
67 | - seaborn
68 | - soundfile
69 | - spacy
70 | - textblob
71 | - tornado
72 | - urllib3
73 | - xarray
74 | - xlrd
75 | - sympy
76 | `;
77 | };
78 |
79 | export const generateTitlePrompt = ({
80 | csvHeaders,
81 | userQuestion,
82 | }: {
83 | csvHeaders?: string[];
84 | userQuestion: string;
85 | }) => {
86 | return `
87 | You are an expert data scientist assistant that creates titles for chat conversations.
88 |
89 | You are given a dataset and a question.
90 |
91 | The dataset has the following columns: ${
92 | csvHeaders?.join(", ") || "[NO HEADERS PROVIDED]"
93 | }
94 |
95 | The question from the user is: ${userQuestion}
96 |
97 | Return ONLY the title of the chat conversation, with no quotes or extra text, and keep it super short (maximum 5 words). Do not return anything else.
98 | `;
99 | };
100 |
101 | export const generateQuestionsPrompt = ({
102 | csvHeaders,
103 | }: {
104 | csvHeaders: string[];
105 | }) =>
106 | `You are an AI assistant that generates questions for data analysis.
107 |
108 | Given the CSV columns: ${csvHeaders.join(", ")}
109 |
110 | Generate exactly 3 insightful questions that can be asked to analyze this data. Focus on questions that would reveal trends, comparisons, or insights.
111 |
112 | Each question should be:
113 | - Direct and concise
114 | - Short enough to fit in a single row
115 | - Without phrases like "in the dataset", "from the data", or "in the CSV file"
116 |
117 | Return ONLY a JSON array of objects, each with "id" (unique string) and "text" (the question string). Do not include any other text, explanations, or the JSON schema.
118 |
119 | Example format:
120 | [{"id": "q1", "text": "What is the average price by category?"}, {"id": "q2", "text": "How many items sold per month?"}]
121 |
122 | Do not wrap the array in any additional object or key like "elements". Return the array directly.`;
123 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | )
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | showCloseButton = true,
53 | ...props
54 | }: React.ComponentProps & {
55 | showCloseButton?: boolean
56 | }) {
57 | return (
58 |
59 |
60 |
68 | {children}
69 | {showCloseButton && (
70 |
74 |
75 | Close
76 |
77 | )}
78 |
79 |
80 | )
81 | }
82 |
83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84 | return (
85 |
90 | )
91 | }
92 |
93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94 | return (
95 |
103 | )
104 | }
105 |
106 | function DialogTitle({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | )
117 | }
118 |
119 | function DialogDescription({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
129 | )
130 | }
131 |
132 | export {
133 | Dialog,
134 | DialogClose,
135 | DialogContent,
136 | DialogDescription,
137 | DialogFooter,
138 | DialogHeader,
139 | DialogOverlay,
140 | DialogPortal,
141 | DialogTitle,
142 | DialogTrigger,
143 | }
144 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Drawer({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function DrawerTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function DrawerPortal({
21 | ...props
22 | }: React.ComponentProps) {
23 | return
24 | }
25 |
26 | function DrawerClose({
27 | ...props
28 | }: React.ComponentProps) {
29 | return
30 | }
31 |
32 | function DrawerOverlay({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | )
46 | }
47 |
48 | function DrawerContent({
49 | className,
50 | children,
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
55 |
56 |
68 |
69 | {children}
70 |
71 |
72 | )
73 | }
74 |
75 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
85 | )
86 | }
87 |
88 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
89 | return (
90 |
95 | )
96 | }
97 |
98 | function DrawerTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | )
109 | }
110 |
111 | function DrawerDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | )
122 | }
123 |
124 | export {
125 | Drawer,
126 | DrawerPortal,
127 | DrawerOverlay,
128 | DrawerTrigger,
129 | DrawerClose,
130 | DrawerContent,
131 | DrawerHeader,
132 | DrawerFooter,
133 | DrawerTitle,
134 | DrawerDescription,
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/DropdownFileActions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | DropdownMenu,
4 | DropdownMenuTrigger,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | } from "@/components/ui/dropdown-menu";
8 | import { UploadedFile } from "@/lib/utils";
9 | import { CsvPreviewModal } from "./CsvPreviewModal";
10 |
11 | export function DropdownFileActions({
12 | uploadedFile,
13 | }: {
14 | uploadedFile?: UploadedFile;
15 | }) {
16 | const [previewOpen, setPreviewOpen] = useState(false);
17 | const hasPreview =
18 | !!uploadedFile?.csvHeaders?.length && !!uploadedFile?.csvRows?.length;
19 |
20 | const hasDownload = !!uploadedFile?.url;
21 |
22 | if (!hasPreview && !hasDownload) return null;
23 |
24 | return (
25 | <>
26 |
27 |
28 |
31 |
32 |
36 | {hasDownload && (
37 |
43 |
44 |
64 |
65 | Download
66 |
67 |
68 |
69 | )}
70 | {hasPreview ? (
71 | setPreviewOpen(true)}
74 | >
75 |
98 | Preview
99 |
100 | ) : null}
101 |
102 |
103 | {hasPreview && (
104 |
110 | )}
111 | >
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default"
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | )
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | )
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | )
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | )
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | )
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | }
186 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { Suspense, useState, useCallback } from "react";
4 | import { Header } from "@/components/header";
5 | import { UploadArea } from "@/components/upload-area";
6 | import { HeroSection } from "@/components/hero-section";
7 | import { QuestionSuggestionCard } from "@/components/question-suggestion-card";
8 | import { extractCsvData } from "@/lib/csvUtils";
9 | import { createChat } from "@/lib/chat-store";
10 | import { useS3Upload } from "next-s3-upload";
11 | import { PromptInput } from "@/components/PromptInput";
12 | import { toast } from "sonner";
13 | import { useLLMModel } from "@/hooks/useLLMModel";
14 | import { redirect } from "next/navigation";
15 | import Loading from "./chat/[id]/loading";
16 |
17 | export interface SuggestedQuestion {
18 | id: string;
19 | text: string;
20 | }
21 |
22 | function CSVToChatClient({
23 | setIsLoading,
24 | }: {
25 | setIsLoading: (load: boolean) => void;
26 | }) {
27 | const { uploadToS3 } = useS3Upload();
28 | const { selectedModelSlug } = useLLMModel();
29 | const [localFile, setLocalFile] = useState(null);
30 | const [suggestedQuestions, setSuggestedQuestions] = useState<
31 | SuggestedQuestion[]
32 | >([]);
33 | const [inputValue, setInputValue] = useState("");
34 | const [isProcessing, setIsProcessing] = useState(false);
35 | const [csvHeaders, setCsvHeaders] = useState([]);
36 | const [csvRows, setCsvRows] = useState<{ [key: string]: string }[]>([]);
37 | const [uploadedFileUrl, setUploadedFileUrl] = useState(null);
38 |
39 | const handleFileUpload = useCallback(async (file: File | null) => {
40 | if (file && file.type === "text/csv") {
41 | setLocalFile(file);
42 | setIsProcessing(true);
43 |
44 | try {
45 | const { headers, sampleRows } = await extractCsvData(file);
46 |
47 | if (headers.length === 0 || sampleRows.length === 0) {
48 | alert("Please upload a CSV with headers.");
49 | setLocalFile(null);
50 | setIsProcessing(false);
51 | return;
52 | }
53 |
54 | setCsvRows(sampleRows);
55 | setCsvHeaders(headers);
56 |
57 | const uploadPromise = uploadToS3(file);
58 |
59 | const response = await fetch("/api/generate-questions", {
60 | method: "POST",
61 | headers: {
62 | "Content-Type": "application/json",
63 | },
64 | body: JSON.stringify({ columns: headers }),
65 | });
66 |
67 | if (!response.ok) {
68 | throw new Error(`HTTP error! status: ${response.status}`);
69 | }
70 |
71 | const uploadedFile = await uploadPromise;
72 |
73 | setUploadedFileUrl(uploadedFile.url);
74 |
75 | const data = await response.json();
76 | setSuggestedQuestions(data.questions);
77 | } catch (error) {
78 | console.error("Failed to process CSV file:", error);
79 | } finally {
80 | setIsProcessing(false);
81 | }
82 | }
83 | }, []);
84 |
85 | const handleSuggestionClick = (question: string) => {
86 | handleSendMessage(question);
87 | };
88 |
89 | const handleSendMessage = async (messageText?: string) => {
90 | const text = messageText || inputValue.trim();
91 | if (!text) return;
92 |
93 | if (!uploadedFileUrl) {
94 | toast.warning("Please upload a CSV file first.");
95 | return;
96 | }
97 |
98 | if (csvHeaders.length === 0) {
99 | toast.warning("Please upload a CSV with headers.");
100 | return;
101 | }
102 |
103 | if (csvRows.length === 0) {
104 | toast.warning("Please upload a CSV with data.");
105 | return;
106 | }
107 |
108 | localStorage.setItem("pendingMessage", text);
109 |
110 | setIsLoading(true);
111 |
112 | const id = await createChat({
113 | userQuestion: text, // it's not stored in db here just used for chat title!
114 | csvHeaders: csvHeaders,
115 | csvFileUrl: uploadedFileUrl,
116 | csvRows: csvRows,
117 | });
118 | redirect(`/chat/${id}?model=${selectedModelSlug}`);
119 | };
120 |
121 | return (
122 | <>
123 |
124 | {/* Large Input Area */}
125 | {localFile && (
126 |
127 | {
131 | handleSendMessage(inputValue);
132 | }}
133 | uploadedFile={{
134 | name: localFile.name,
135 | csvHeaders: csvHeaders,
136 | csvRows: csvRows,
137 | }}
138 | textAreaClassName="h-[88px] md:h-[100px]"
139 | isLLMAnswering={false}
140 | onStopLLM={() => {}}
141 | />
142 |
143 | )}
144 | {/* Processing State */}
145 | {isProcessing && (
146 |
147 |
148 | Generating suggestions{" "}
149 | ...
150 |
151 |
152 | {Array(3)
153 | .fill(null)
154 | .map((_, idx) => (
155 |
156 | ))}
157 |
158 |
159 | )}
160 | {/* Suggestions */}
161 | {suggestedQuestions.length > 0 && !isProcessing && (
162 |
163 |
164 | Suggestions{" "}
165 | based on your uploaded CSV:
166 |
167 |
168 | {suggestedQuestions.map((suggestion) => (
169 | handleSuggestionClick(suggestion.text)}
173 | />
174 | ))}
175 |
176 |
177 | )}
178 | >
179 | );
180 | }
181 |
182 | export default function CSVToChat() {
183 | const [isLoading, setIsLoading] = useState(false);
184 |
185 | if (isLoading) {
186 | return ;
187 | }
188 |
189 | return (
190 |
191 |
192 |
193 |
194 |
195 |
196 | Loading...}>
197 |
198 |
199 |
200 |
201 | );
202 | }
203 |
--------------------------------------------------------------------------------
/public/together.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/src/components/ChatHistoryMenu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState, useEffect, Fragment } from "react";
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuTrigger,
9 | DropdownMenuContent,
10 | } from "@/components/ui/dropdown-menu";
11 | import {
12 | Drawer,
13 | DrawerTrigger,
14 | DrawerContent,
15 | DrawerTitle,
16 | } from "@/components/ui/drawer";
17 | import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
18 |
19 | export function ChatHistoryMenu({ chatId }: { chatId?: string }) {
20 | const [drawerOpen, setDrawerOpen] = useState(false);
21 | const pathname = usePathname();
22 | const [chatLinks, setChatLinks] = useState<{ id: string; title: string }[]>(
23 | []
24 | );
25 | const [isLoading, setLoading] = useState(true);
26 |
27 | // Badge component for notification bubble
28 | const ChatBadge = ({ count }: { count: number }) => {
29 | if (count <= 0) return null;
30 | const displayCount = count > 99 ? "99+" : count;
31 | return (
32 |
55 | {displayCount}
56 |
57 | );
58 | };
59 |
60 | // Track visited chat ids in localStorage
61 | useEffect(() => {
62 | if (typeof window !== "undefined" && chatId) {
63 | const key = "visitedChatIds";
64 | let ids: string[] = [];
65 | try {
66 | ids = JSON.parse(localStorage.getItem(key) || "[]");
67 | } catch {}
68 | if (!ids.includes(chatId)) {
69 | ids.push(chatId);
70 | localStorage.setItem(key, JSON.stringify(ids));
71 | }
72 | }
73 | }, [chatId]);
74 |
75 | useEffect(() => {
76 | if (typeof window === "undefined") return;
77 | const key = "visitedChatIds";
78 | let ids: string[] = [];
79 | try {
80 | ids = JSON.parse(localStorage.getItem(key) || "[]");
81 | } catch {}
82 | if (ids.length === 0) {
83 | setLoading(false);
84 | return;
85 | }
86 | // Fetch chat metadata from backend
87 | fetch("/api/chat/history", {
88 | method: "POST",
89 | headers: { "Content-Type": "application/json" },
90 | body: JSON.stringify({ ids }),
91 | })
92 | .then((res) => res.json())
93 | .then((data) => {
94 | if (Array.isArray(data)) setChatLinks(data);
95 | })
96 | .finally(() => setLoading(false));
97 | }, []);
98 |
99 | const HistoryLinks = () => {
100 | return (
101 |
102 | {chatLinks.map((chat) => {
103 | const href = `/chat/${chat.id}`;
104 | const isActive = pathname === href;
105 | return (
106 |
107 | setDrawerOpen(false)}
111 | className={`text-sm text-left py-2.5 px-6 md:py-2 md:px-4 ${
112 | isActive
113 | ? "bg-slate-200 rounded font-medium text-[#0f172b]"
114 | : "text-[#314158]"
115 | }`}
116 | >
117 | {chat.title}
118 |
119 |
120 | );
121 | })}
122 |
123 | );
124 | };
125 |
126 | if (isLoading) {
127 | return (
128 |
129 |
140 |
141 | );
142 | }
143 |
144 | if (chatLinks.length === 0) {
145 | return (
146 |
147 |
158 |
159 | );
160 | }
161 |
162 | return (
163 | <>
164 | {/* Desktop: DropdownMenu, Mobile: Drawer */}
165 |
166 |
167 |
168 |
178 |
179 |
180 |
181 | Chat History
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
200 |
201 |
202 |
203 | Chat History
204 |
205 |
206 |
215 |
216 |
217 |
218 |
219 |
220 | >
221 | );
222 | }
223 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function DropdownMenu({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DropdownMenuPortal({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | )
21 | }
22 |
23 | function DropdownMenuTrigger({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
31 | )
32 | }
33 |
34 | function DropdownMenuContent({
35 | className,
36 | sideOffset = 4,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
41 |
50 |
51 | )
52 | }
53 |
54 | function DropdownMenuGroup({
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
59 | )
60 | }
61 |
62 | function DropdownMenuItem({
63 | className,
64 | inset,
65 | variant = "default",
66 | ...props
67 | }: React.ComponentProps & {
68 | inset?: boolean
69 | variant?: "default" | "destructive"
70 | }) {
71 | return (
72 |
82 | )
83 | }
84 |
85 | function DropdownMenuCheckboxItem({
86 | className,
87 | children,
88 | checked,
89 | ...props
90 | }: React.ComponentProps) {
91 | return (
92 |
101 |
102 |
103 |
104 |
105 |
106 | {children}
107 |
108 | )
109 | }
110 |
111 | function DropdownMenuRadioGroup({
112 | ...props
113 | }: React.ComponentProps) {
114 | return (
115 |
119 | )
120 | }
121 |
122 | function DropdownMenuRadioItem({
123 | className,
124 | children,
125 | ...props
126 | }: React.ComponentProps) {
127 | return (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | )
144 | }
145 |
146 | function DropdownMenuLabel({
147 | className,
148 | inset,
149 | ...props
150 | }: React.ComponentProps & {
151 | inset?: boolean
152 | }) {
153 | return (
154 |
163 | )
164 | }
165 |
166 | function DropdownMenuSeparator({
167 | className,
168 | ...props
169 | }: React.ComponentProps) {
170 | return (
171 |
176 | )
177 | }
178 |
179 | function DropdownMenuShortcut({
180 | className,
181 | ...props
182 | }: React.ComponentProps<"span">) {
183 | return (
184 |
192 | )
193 | }
194 |
195 | function DropdownMenuSub({
196 | ...props
197 | }: React.ComponentProps) {
198 | return
199 | }
200 |
201 | function DropdownMenuSubTrigger({
202 | className,
203 | inset,
204 | children,
205 | ...props
206 | }: React.ComponentProps & {
207 | inset?: boolean
208 | }) {
209 | return (
210 |
219 | {children}
220 |
221 |
222 | )
223 | }
224 |
225 | function DropdownMenuSubContent({
226 | className,
227 | ...props
228 | }: React.ComponentProps) {
229 | return (
230 |
238 | )
239 | }
240 |
241 | export {
242 | DropdownMenu,
243 | DropdownMenuPortal,
244 | DropdownMenuTrigger,
245 | DropdownMenuContent,
246 | DropdownMenuGroup,
247 | DropdownMenuLabel,
248 | DropdownMenuItem,
249 | DropdownMenuCheckboxItem,
250 | DropdownMenuRadioGroup,
251 | DropdownMenuRadioItem,
252 | DropdownMenuSeparator,
253 | DropdownMenuShortcut,
254 | DropdownMenuSub,
255 | DropdownMenuSubTrigger,
256 | DropdownMenuSubContent,
257 | }
258 |
--------------------------------------------------------------------------------
/public/products-100.csv:
--------------------------------------------------------------------------------
1 | Index,Name,Description,Brand,Category,Price,Currency,Stock,EAN,Color,Size,Availability,Internal ID
2 | 1,Compact Printer Air Advanced Digital,Situation organization these memory much off.,"Garner, Boyle and Flynn",Books & Stationery,265,USD,774,2091465262179,ForestGreen,Large,pre_order,56
3 | 2,Tablet,Discussion loss politics free one thousand.,Mueller Inc,Shoes & Footwear,502,USD,81,5286196620740,Black,8x10 in,in_stock,29
4 | 3,Smart Blender Cooker,No situation per.,"Lawson, Keller and Winters",Kitchen Appliances,227,USD,726,1282898648918,SlateGray,XS,in_stock,70
5 | 4,Advanced Router Rechargeable,For force gas energy six laugh.,Gallagher and Sons,Kitchen Appliances,121,USD,896,3879177514583,PaleGreen,L,discontinued,31
6 | 5,Portable Mouse Monitor Phone,Feeling back religious however author room scientist.,Irwin LLC,Kids' Clothing,1,USD,925,9055773261265,SeaShell,100x200 mm,discontinued,10
7 | 6,Radio,Character prove growth contain serious customer.,"Benjamin, Nelson and Hancock",Skincare,426,USD,549,1150028980156,CornflowerBlue,30x40 cm,pre_order,60
8 | 7,Ultra Projector Oven Thermostat Prime Advanced,Pattern possible look necessary indicate work nearly.,"Mccoy, Waters and Rose",Laptops & Computers,68,USD,870,5029747624534,Purple,S,discontinued,86
9 | 8,Webcam Stove Grill,Deep area join carry age.,Morrow and Sons,Automotive,159,USD,584,9883725074294,MediumOrchid,8x10 in,pre_order,50
10 | 9,Eco Radio,Know father for act let.,"Edwards, Odonnell and Conley",Skincare,454,USD,499,1773215338624,DimGray,Medium,pre_order,88
11 | 10,Ultra Powerbank Brush Charger Max,Meeting add economic task outside.,"Brennan, Archer and Rosales",Fitness Equipment,845,USD,564,4877111475333,Silver,Extra Large,discontinued,88
12 | 11,Wireless Dock,Whole bill sound whether will.,Russo-West,Health & Wellness,257,USD,168,9680811891595,Wheat,Extra Large,pre_order,30
13 | 12,Fast Camera Router Fan Smart,Various above trouble itself crime form.,Vazquez and Sons,Cleaning Supplies,221,USD,672,8567222480604,MidnightBlue,Medium,pre_order,32
14 | 13,Heater Radio,Ten do evidence billion perhaps read bank enter.,Delgado-Blackwell,Cycling,689,USD,156,5686660235911,Orange,L,pre_order,57
15 | 14,Smart Trimmer Webcam Heater,Example available better.,Contreras PLC,Automotive,279,USD,818,1941032767914,Gainsboro,XXL,pre_order,66
16 | 15,Webcam Dock Heater,State yet soon traditional your deal.,"Juarez, Powell and Travis",Cleaning Supplies,101,USD,159,1438856474369,Sienna,30x40 cm,pre_order,88
17 | 16,Automatic Watch Lite Sense,As east entire positive source their.,"Reid, Chase and Ballard",Team Sports,316,USD,329,3500722785805,Tan,5x7 in,limited_stock,44
18 | 17,Eco Iron Monitor Air,Color single indeed yard event popular food boy.,Barker-Murphy,Automotive,982,USD,47,7887280730963,OliveDrab,XL,out_of_stock,79
19 | 18,Digital Tablet Router Printer Lite,Accept campaign every research test.,Vazquez-Smith,Health & Wellness,3,USD,732,4170125892616,LemonChiffon,10x10 cm,backorder,87
20 | 19,Eco Freezer Powerbank Watch,Of price visit interesting enter three set.,"Pruitt, Higgins and Barnett",Cleaning Supplies,340,USD,618,6340907128242,Cyan,S,limited_stock,21
21 | 20,Cooler Fan,Fall billion city share.,Bray LLC,Smartphones,529,USD,844,0267078141619,Bisque,XS,backorder,22
22 | 21,Portable Camera Plus Air Ultra,Reality hair explain up bad.,Wang-Valenzuela,Women's Clothing,197,USD,163,0963840013930,Magenta,L,in_stock,96
23 | 22,Wireless Powerbank 360 Advanced Ultra,Deal head decade age outside military culture.,Carney Ltd,Bedding & Bath,677,USD,418,3918763032312,Beige,M,out_of_stock,90
24 | 23,Rechargeable Lamp Speakerphone Headphones,Effort own safe main walk quickly.,"Gallegos, Osborne and Carpenter",Skincare,664,USD,577,1824359393441,DarkBlue,L,pre_order,26
25 | 24,Mini Scooter Microphone,Maybe around expect own whether pay.,"Douglas, Thornton and Soto",Grooming Tools,784,USD,510,9839666739792,DimGray,L,backorder,55
26 | 25,Rechargeable Webcam Stove Grill,Movement you Mr decide history effect.,Horn-Pope,Health & Wellness,900,USD,623,6501687247749,PaleGreen,5x7 in,out_of_stock,47
27 | 26,Mini Iron Drone Wireless Pro,From Congress low.,Khan-Parrish,Cycling,909,USD,797,9496081864722,CadetBlue,XXL,limited_stock,93
28 | 27,Portable Freezer Phone Automatic Smart,Live fish audience piece his ago true.,Burton PLC,Automotive,350,USD,756,3486711401836,Snow,XL,pre_order,61
29 | 28,Wireless Webcam Stove Grill,Pull at improve health also animal forward person.,Henry Group,Clothing & Apparel,858,USD,912,7097613907430,LightSalmon,50x70 cm,limited_stock,43
30 | 29,Clock Brush,Other another must explain send somebody consider.,Larson-Spence,Furniture,985,USD,180,8301096575048,SaddleBrown,Extra Large,in_stock,40
31 | 30,Keyboard Toaster Monitor,My head prove exist change.,Bonilla-Spears,Kids' Clothing,576,USD,988,6639325474032,DarkSlateGray,L,pre_order,40
32 | 31,Silent Printer Fan Shaver,Region week yet weight.,Love LLC,Sports & Outdoors,102,USD,669,5564792899662,MediumSpringGreen,L,limited_stock,14
33 | 32,Wireless Scooter Clean Mini,Green main imagine way suffer wall.,Terry-Oliver,Fragrances,283,USD,343,9921752342730,MintCream,S,pre_order,84
34 | 33,Advanced Microphone Cooler Eco,Most other newspaper beyond real.,"Morales, Weaver and Fernandez",Smartphones,904,USD,532,7107438924403,LightCyan,Extra Large,limited_stock,86
35 | 34,Automatic Brush Fast Eco,Record response relationship.,Newman Ltd,Kids' Clothing,407,USD,285,2327483415120,SeaGreen,8x10 in,out_of_stock,65
36 | 35,Premium Bicycle Microphone,Congress how list third rise.,"Novak, Archer and Walton",Shoes & Footwear,480,USD,364,6209821998907,AliceBlue,Extra Large,discontinued,78
37 | 36,Smart Phone Scooter Clean,Reality kid create thought window.,Marshall-Dougherty,Women's Clothing,635,USD,727,7883295446066,MediumVioletRed,10x10 cm,backorder,74
38 | 37,Bicycle,Garden mother before ten different increase.,Hays-Cunningham,Smartphones,234,USD,394,7670612815694,White,10x10 cm,backorder,48
39 | 38,Ultra Dock Trimmer Automatic Edge,Do customer stage act send.,Barnes-Glass,Kids' Clothing,640,USD,780,2837107354495,LightBlue,5x7 in,discontinued,96
40 | 39,Wireless Tablet Router Printer Wireless Premium Air,North meeting short summer situation positive candidate.,Meyers-Hess,Sports & Outdoors,781,USD,947,8247903213713,BlanchedAlmond,XL,out_of_stock,69
41 | 40,Clean Printer Advanced Premium Sense,Bag away always often join seat direction.,Williamson PLC,Haircare,913,USD,820,5234087663802,Moccasin,Small,discontinued,77
42 | 41,Silent Dock Fast,Speak not hospital bit part.,"May, Hendrix and Robbins",Fishing & Hunting,100,USD,802,0938364240247,CadetBlue,M,limited_stock,12
43 | 42,Ultra Speakerphone Oven Go Smart X,Value bill yeah tell phone.,"Zimmerman, Barr and Davis",Office Supplies,999,USD,853,0450118214736,MediumOrchid,12x18 in,pre_order,23
44 | 43,Rechargeable Projector Pro,Bank everything ago girl.,"Levine, Martin and Mccann",Home Decor,935,USD,55,0655296358115,SlateBlue,XL,limited_stock,52
45 | 44,Eco Vacuum,Bit name seat sea.,Werner PLC,Health & Wellness,259,USD,668,8611986118980,LightCoral,5x7 in,in_stock,45
46 | 45,Smart Grill Tablet Fridge Go Smart Smart,By result ago born become.,"Huff, Cameron and Stephenson",Fitness Equipment,488,USD,966,9005775281402,Cornsilk,10x10 cm,out_of_stock,62
47 | 46,Digital Stove Silent Pro,Suffer range between step mention peace prepare.,"Suarez, Riddle and Sanders",Health & Wellness,161,USD,349,5367012957872,OrangeRed,Extra Large,limited_stock,9
48 | 47,Portable Powerbank X Air,Present court note medical bed red movie.,Hanson-Schultz,Camping & Hiking,599,USD,263,6854117854742,Moccasin,Large,discontinued,17
49 | 48,Clock,Onto story what job require.,"Malone, Jacobson and Hudson",Team Sports,731,USD,664,1382809012286,FloralWhite,S,limited_stock,55
50 | 49,Radio Treadmill,Through choose record prove happen.,Barron-Little,Office Supplies,511,USD,395,5461960384619,Violet,XL,discontinued,14
51 | 50,Ultra Cooler Treadmill Touch Pro Ultra,Certainly simple light safe child PM candidate.,Bonilla-Oconnell,Fragrances,214,USD,574,3625664059934,NavajoWhite,XXL,pre_order,33
52 | 51,Mini Fridge Camera,Little determine at huge month.,Decker and Sons,Fitness Equipment,777,USD,832,4894233116968,PaleGoldenRod,Extra Large,in_stock,16
53 | 52,Rechargeable Tablet Plus Portable Mini,Expect question how sound.,Mooney-Gonzales,Fragrances,820,USD,535,8183922928019,Cyan,100x200 mm,in_stock,19
54 | 53,Tablet,Many deal community public beyond safe anyone.,"Blake, Weiss and Montgomery",Beauty & Personal Care,408,USD,922,8183313115592,Chocolate,Medium,backorder,34
55 | 54,Wireless Light Toaster Lite Touch Eco,Oil guy table industry hand which.,Singleton PLC,Bedding & Bath,791,USD,827,3057639268476,Fuchsia,50x70 cm,in_stock,64
56 | 55,Scooter,Small approach possible choose according.,Rivas Group,Cameras & Accessories,370,USD,601,1974896712011,DarkOrchid,L,backorder,46
57 | 56,Advanced Camera Heater Webcam X Ultra Prime,Audience bit past central film each.,Cervantes Ltd,Office Supplies,224,USD,372,0983367163970,Peru,50x70 cm,discontinued,56
58 | 57,Ultra Keyboard Compact Eco,Bit responsibility cover him mean call civil.,Pace-Hodges,Books & Stationery,514,USD,491,8481605893389,LightGray,8x10 in,backorder,19
59 | 58,Portable Scale Speaker Powerbank Clean,Throughout special new you view season within.,Morris LLC,Women's Clothing,689,USD,332,5175038823985,PowderBlue,Small,backorder,94
60 | 59,Oven Speaker Fan,Hot behavior traditional.,Schwartz Ltd,Men's Clothing,489,USD,423,2567562782853,GoldenRod,Small,pre_order,99
61 | 60,Automatic Speaker Router Lamp Prime,Word score education thousand high treatment.,"Sexton, Dickerson and Blair",Grooming Tools,621,USD,667,6744198567221,FireBrick,5x7 in,in_stock,61
62 | 61,Smart Webcam Projector Lock,Subject agree you off.,Stokes Inc,Men's Clothing,440,USD,973,4811928042234,SteelBlue,Medium,in_stock,3
63 | 62,Fast Fan,Avoid very final food scene possible.,Ali-Oliver,Cleaning Supplies,982,USD,804,3388615003133,DarkOrchid,100x200 mm,backorder,70
64 | 63,Automatic Cooler Edge,Career for much exist.,"Odonnell, Boyle and Oconnor",Beauty & Personal Care,124,USD,513,6261793125392,LimeGreen,50x70 cm,pre_order,83
65 | 64,Advanced Freezer Advanced Advanced,Chance material himself soldier.,Gill LLC,Office Supplies,489,USD,561,6474143371206,Tomato,S,pre_order,48
66 | 65,Silent Trimmer Shaver Fast,Task point seat better.,Munoz Group,Smartphones,130,USD,876,4089415274929,DarkSlateGray,5x7 in,in_stock,76
67 | 66,Treadmill,Vote only run modern.,"Frost, Christensen and Burnett",Women's Clothing,731,USD,744,4598541150958,Cyan,5x7 in,out_of_stock,70
68 | 67,Mini Charger Lock Oven Sense Sense,Major tell him share allow.,"Burton, Gross and Giles",Haircare,750,USD,623,9282813513019,Olive,12x18 in,in_stock,81
69 | 68,Thermostat Trimmer,Drug interview hotel decide pattern.,"Hester, Love and Lynch","Accessories (Bags, Hats, Belts)",206,USD,677,8282848236311,MidnightBlue,8x10 in,discontinued,6
70 | 69,Clean Iron Premium Air Wireless,Around tough popular present sign herself pattern.,"Shepherd, Greene and House",Beauty & Personal Care,293,USD,700,7935749142557,GhostWhite,XXL,out_of_stock,5
71 | 70,Compact Mouse Mouse Router Mini,Occur miss son Democrat happy.,Barry PLC,Smartwatches,5,USD,247,0252926529077,PapayaWhip,10x10 cm,limited_stock,79
72 | 71,Premium Lock Mini Advanced,Three character traditional maybe.,"Elliott, Mcfarland and Duran",Automotive,12,USD,439,3997003361528,Plum,XS,pre_order,40
73 | 72,Silent Webcam Webcam Treadmill Sense Portable Advanced,Leg my mother father.,George-Day,Makeup,148,USD,650,6908065460286,Aqua,M,backorder,29
74 | 73,Compact Thermostat Oven,Focus that consumer amount half.,Hull Inc,Home & Kitchen,685,USD,747,7695934105636,SaddleBrown,30x40 cm,in_stock,36
75 | 74,Wireless Watch Tablet Printer Automatic Eco Sense,Might poor animal must protect blue forward.,Fleming Inc,Smartwatches,59,USD,169,1089969929675,Sienna,Large,out_of_stock,31
76 | 75,Automatic Blender,Smile human machine section bank.,Mahoney-Bryan,Home Decor,630,USD,438,0310774792552,LightGoldenRodYellow,8x10 in,backorder,4
77 | 76,Rechargeable Webcam Dock Heater,Organization address section collection church gun consumer do.,Kennedy-Gordon,Cycling,800,USD,151,1340784358003,Brown,XXL,in_stock,66
78 | 77,Clean Toaster Oven Air Touch,Heavy threat beautiful material assume piece.,Olsen-Sawyer,Kids' Clothing,365,USD,71,7806787405877,LimeGreen,Medium,pre_order,40
79 | 78,Brush,Control daughter effect investment piece training nation.,"Lowe, Mosley and Rivera",Bedding & Bath,273,USD,623,0168419438521,RoyalBlue,S,in_stock,42
80 | 79,Portable Kettle,Apply out industry must tell.,Sampson-Leon,Home & Kitchen,50,USD,413,0445370808496,MediumAquaMarine,L,pre_order,25
81 | 80,Rechargeable Brush Compact,Early century every amount than past.,Castro-Mccarty,Automotive,748,USD,326,6855062072649,Sienna,S,out_of_stock,81
82 | 81,Fast Heater Cooker Compact Silent,Strategy fund mind rather change.,"Decker, Montes and Logan",Makeup,367,USD,19,3868467036959,Orchid,8x10 in,in_stock,15
83 | 82,Mini Drone X Portable,Shoulder cost especially.,Whitney Group,Books & Stationery,352,USD,123,9437200314292,BlueViolet,Medium,backorder,12
84 | 83,Heater Keyboard,Remember here kind enjoy source room reflect be.,Brooks and Sons,Office Supplies,103,USD,208,5328785590239,Chocolate,Extra Large,backorder,72
85 | 84,Wireless Fan Thermostat Max Automatic Max,Design price for especially section college over green.,Kaiser-Banks,Books & Stationery,423,USD,759,1396411470228,MidnightBlue,5x7 in,in_stock,81
86 | 85,Fast Keyboard,Success other institution fear.,Lewis Ltd,Camping & Hiking,998,USD,10,3163238295239,Cyan,50x70 cm,limited_stock,64
87 | 86,Ultra Mixer Toaster Toaster Smart,Much guess have pattern southern true hair miss.,Galloway and Sons,Women's Clothing,342,USD,221,4862330962177,Tomato,S,pre_order,88
88 | 87,Silent Speakerphone Scanner Monitor,Quality yet significant lawyer face field yet realize.,Cowan Inc,Fragrances,593,USD,580,1710131812265,Navy,XXL,discontinued,20
89 | 88,Silent Scanner Monitor Treadmill,Do interesting suggest agreement range admit rule.,"Nichols, Howe and Miller",Health & Wellness,30,USD,728,1287056757693,Wheat,10x10 cm,pre_order,60
90 | 89,Fast Vacuum Cooler,Explain six firm recent rock skin.,Blair-Russell,Sports & Outdoors,307,USD,320,4874329354658,LightSalmon,L,limited_stock,5
91 | 90,Digital Trimmer,Ago floor nice member wait.,Christian-Tanner,Laptops & Computers,541,USD,488,4147810179628,SteelBlue,M,pre_order,31
92 | 91,Automatic Trimmer Sense,Much table ground information news senior.,Harrington-Valentine,Kitchen Appliances,405,USD,950,0000524638185,SlateGray,8x10 in,in_stock,85
93 | 92,Portable Scooter Wireless Digital,Thousand various evidence.,Bauer Inc,Cameras & Accessories,519,USD,495,4564280934296,DeepSkyBlue,Large,in_stock,36
94 | 93,Fridge Cooker,Try better loss pretty special.,Jackson Group,Furniture,393,USD,499,1861389468671,Black,Small,in_stock,80
95 | 94,Scanner,Draw material quickly most.,"Wang, Ayala and Bowen",Cleaning Supplies,472,USD,63,7794860625802,ForestGreen,12x18 in,backorder,8
96 | 95,Ultra Radio Radio,Shoulder red boy away if.,Maldonado-Ritter,Haircare,382,USD,639,3611080124547,Aquamarine,12x18 in,pre_order,48
97 | 96,Router,Investment everything story buy.,Atkinson Inc,Laptops & Computers,375,USD,703,3330396409604,Gainsboro,8x10 in,limited_stock,55
98 | 97,Fast Thermostat Microphone Scooter,Nature notice themselves news.,Shea Ltd,Beauty & Personal Care,289,USD,727,9265397743935,White,Extra Large,backorder,25
99 | 98,Eco Heater Toaster Stove Silent Sense,Understand still or summer rule.,West Ltd,Cleaning Supplies,232,USD,998,5505866454530,OldLace,Small,limited_stock,20
100 | 99,Clean Blender Scale Lite,Final art some push.,"Bell, Gamble and Barrett",Camping & Hiking,241,USD,391,3893594435450,Aquamarine,5x7 in,limited_stock,5
101 | 100,Smart Lamp,However public major baby.,"Hebert, Hughes and Trujillo",Bedding & Bath,71,USD,518,8264263605712,Brown,Medium,limited_stock,3
102 |
--------------------------------------------------------------------------------
/src/components/chat-screen.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useChat } from "@ai-sdk/react";
4 | import React, { useEffect, useState, useRef } from "react";
5 | import { Header } from "@/components/header";
6 | import { ChatInput } from "@/components/ChatInput";
7 | import { MemoizedMarkdown } from "./MemoizedMarkdown";
8 | import { TogetherCodeInterpreterResponseData } from "@/lib/coding";
9 | import { type UIMessage } from "ai";
10 | import { ImageFigure } from "./chatTools/ImageFigure";
11 | import { TerminalOutput } from "./chatTools/TerminalOutput";
12 | import { ErrorOutput } from "./chatTools/ErrorOutput";
13 | import { useAutoScroll } from "../hooks/useAutoScroll";
14 | import { useDraftedInput } from "../hooks/useDraftedInput";
15 | import { DbMessage } from "@/lib/chat-store";
16 | import {
17 | cn,
18 | extractCodeFromText,
19 | formatLLMTimestamp,
20 | UploadedFile,
21 | } from "@/lib/utils";
22 | import { ErrorBanner } from "./ui/ErrorBanner";
23 | import { ThinkingIndicator } from "./ui/ThinkingIndicator";
24 | import ReasoningAccordion from "./ReasoningAccordion";
25 | import { useLLMModel } from "@/hooks/useLLMModel";
26 | import { CodeRunning } from "./chatTools/CodeRunning";
27 | import { CHAT_MODELS } from "@/lib/models";
28 | import { useRouter } from "next/navigation";
29 |
30 | export type Message = UIMessage & {
31 | isThinking?: boolean;
32 | isUser?: boolean;
33 | toolCall?: {
34 | toolInvocation: {
35 | toolName: string;
36 | args: string;
37 | state: string;
38 | result?: any;
39 | };
40 | };
41 | duration?: number; // Duration in seconds for LLM/coding
42 | model?: string; // Model used for the message
43 | isAutoErrorResolution?: boolean; // Added for auto error resolution prompt
44 | };
45 |
46 | export function ChatScreen({
47 | uploadedFile,
48 | id,
49 | initialMessages,
50 | }: {
51 | uploadedFile: UploadedFile;
52 | id?: string;
53 | initialMessages?: DbMessage[];
54 | }) {
55 | const router = useRouter();
56 | const { selectedModelSlug } = useLLMModel();
57 |
58 | const modelContextLength = CHAT_MODELS.find(
59 | (model) => model.slug === selectedModelSlug
60 | )?.contextLength;
61 |
62 | const { messages, setMessages, append, stop, status } = useChat({
63 | id, // use the provided chat ID
64 | initialMessages: initialMessages || [], // initial messages if provided
65 | sendExtraMessageFields: true, // send id and createdAt for each message
66 | experimental_prepareRequestBody({ messages, id }) {
67 | return {
68 | message: messages[messages.length - 1].content,
69 | id,
70 | model: selectedModelSlug,
71 | };
72 | },
73 | // Fake tool call
74 | onFinish: async (message) => {
75 | const code = extractCodeFromText(message.content);
76 |
77 | if (code) {
78 | // Add a "tool-invocation" message with a "start" state
79 | setMessages((prev) => {
80 | return [
81 | ...prev,
82 | {
83 | id: message.id + "_tool_call", // Unique ID for the tool call message
84 | role: "assistant",
85 | content: "",
86 | isThinking: true,
87 | toolCall: {
88 | toolInvocation: {
89 | toolName: "runCode",
90 | args: code,
91 | state: "start",
92 | },
93 | },
94 | },
95 | ];
96 | });
97 |
98 | setIsCodeRunning(true);
99 | codeAbortController.current = new AbortController();
100 | let result;
101 | try {
102 | const response = await fetch("/api/coding", {
103 | method: "POST",
104 | headers: {
105 | "Content-Type": "application/json",
106 | },
107 | body: JSON.stringify({ code, id }),
108 | signal: codeAbortController.current.signal,
109 | });
110 |
111 | result = await response.json();
112 | } catch (error: any) {
113 | if (error.name === "AbortError") {
114 | // Fetch was aborted, handle accordingly
115 | setIsCodeRunning(false);
116 | // Optionally update the tool call message to show cancellation
117 | setMessages((prev) =>
118 | prev.map((msg) => {
119 | if (msg.id === message.id + "_tool_call") {
120 | return {
121 | ...msg,
122 | isThinking: false,
123 | content: "Code execution cancelled.",
124 | toolCall: {
125 | toolInvocation: {
126 | toolName: "runCode",
127 | args: code,
128 | state: "result",
129 | result: { outputs: [] },
130 | },
131 | },
132 | };
133 | }
134 | return msg;
135 | })
136 | );
137 | return;
138 | } else {
139 | // Handle other errors
140 | setIsCodeRunning(false);
141 | setMessages((prev) =>
142 | prev.map((msg) => {
143 | if (msg.id === message.id + "_tool_call") {
144 | return {
145 | ...msg,
146 | isThinking: false,
147 | content: `Code execution failed: ${error.message}`,
148 | toolCall: {
149 | toolInvocation: {
150 | toolName: "runCode",
151 | args: code,
152 | state: "result",
153 | result: {
154 | outputs: [{ type: "error", data: error.message }],
155 | },
156 | },
157 | },
158 | };
159 | }
160 | return msg;
161 | })
162 | );
163 | return;
164 | }
165 | }
166 |
167 | // Check for error in outputs
168 | const errorOutput = Array.isArray(result.outputs)
169 | ? result.outputs.find(
170 | (output: any) =>
171 | output.type === "error" || output.type === "stderr"
172 | )
173 | : undefined;
174 | const errorOccurred = Boolean(errorOutput);
175 | const errorMessage = errorOutput
176 | ? errorOutput.data || "Unknown error"
177 | : "";
178 |
179 | if (errorOccurred) {
180 | // Send error back to AI for resolution
181 | const errorResolutionPrompt = `The following error occurred when running the code you provided: ${errorMessage}. Please try to fix the code and try again.`;
182 |
183 | // Append the error resolution prompt as a user message
184 | setTimeout(() => {
185 | append(
186 | {
187 | role: "user",
188 | content: errorResolutionPrompt,
189 | },
190 | {
191 | headers: {
192 | "Content-Type": "application/json",
193 | "X-Auto-Error-Resolved": "true",
194 | },
195 | }
196 | );
197 | }, 1000); // slight delay for UX
198 | }
199 |
200 | // Update the tool call message with the "result" state
201 | setMessages((prev) => {
202 | return prev.map((msg) => {
203 | if (msg.id === message.id + "_tool_call") {
204 | return {
205 | ...msg,
206 | isThinking: false,
207 | content: errorOccurred
208 | ? "Code execution failed."
209 | : "Code execution complete.",
210 | toolCall: {
211 | toolInvocation: {
212 | toolName: "runCode",
213 | args: code,
214 | state: "result",
215 | result: result,
216 | },
217 | },
218 | };
219 | }
220 | return msg;
221 | });
222 | });
223 | setIsCodeRunning(false);
224 | codeAbortController.current = null;
225 | }
226 | },
227 | });
228 |
229 | // On mount, check for pendingMessage in localStorage and append it if present
230 | const didAppendPending = React.useRef(false);
231 | useEffect(() => {
232 | if (
233 | !didAppendPending.current &&
234 | messages.length === 0 &&
235 | typeof window !== "undefined"
236 | ) {
237 | const pending = localStorage.getItem("pendingMessage");
238 | if (pending) {
239 | append({
240 | role: "user",
241 | content: pending,
242 | });
243 | localStorage.removeItem("pendingMessage");
244 | didAppendPending.current = true;
245 | }
246 | }
247 | }, [append, messages.length]);
248 |
249 | // Use a unique key for each chat window's draft input
250 | const [inputValue, setInputValue, clearInputValue] = useDraftedInput(
251 | id ? `chatInputDraft-${id}` : "chatInputDraft"
252 | );
253 |
254 | const [isCodeRunning, setIsCodeRunning] = useState(false);
255 | const codeAbortController = useRef(null);
256 | const { messagesContainerRef, messagesEndRef, isUserAtBottom } =
257 | useAutoScroll({ status, isCodeRunning });
258 |
259 | // Token counting logic (approximate: 1 token ≈ 4 chars)
260 | const [tokenInfo, setTokenInfo] = useState({
261 | tokens: 0,
262 | percent: 0,
263 | max: modelContextLength || 0,
264 | });
265 |
266 | useEffect(() => {
267 | if (!modelContextLength) return;
268 | // Only count user/assistant messages (not tool calls)
269 | const text = messages
270 | .filter((m) => m.role === "user" || m.role === "assistant")
271 | .map((m) => m.content)
272 | .join("\n");
273 | // Approximate token count: 1 token ≈ 3 chars
274 | const approxTokens = Math.ceil(text.length / 3);
275 | const percent = Math.min(100, (approxTokens / modelContextLength) * 100);
276 | setTokenInfo({ tokens: approxTokens, percent, max: modelContextLength });
277 | }, [messages, modelContextLength]);
278 |
279 | return (
280 |
281 |
282 |
283 | {/* Context usage bar */}
284 |
285 |
286 |
287 | Context used
288 |
289 | {tokenInfo.tokens} / {tokenInfo.max} tokens (
290 | {tokenInfo.percent.toFixed(1)}%)
291 |
292 |
293 |
299 |
300 |
301 |
302 |
303 | {/* Messages */}
304 |
308 | {messages.map((message, messageIdx) => {
309 | const currentMessage = message as Message; // Cast to our custom Message interface
310 |
311 | const codeResults =
312 | currentMessage.toolCall?.toolInvocation.toolName === "runCode"
313 | ? (currentMessage.toolCall?.toolInvocation
314 | .result as TogetherCodeInterpreterResponseData)
315 | : undefined;
316 |
317 | const stdOut = codeResults?.outputs?.find(
318 | (result: any) => result.type === "stdout"
319 | );
320 |
321 | const errorCode = codeResults?.outputs?.find(
322 | (result: any) =>
323 | result.type === "error" || result.type === "stderr"
324 | );
325 |
326 | const imagePngBase64 = codeResults?.outputs?.find(
327 | (result: any) =>
328 | result.type === "display_data" &&
329 | result.data &&
330 | result.data["image/png"]
331 | );
332 |
333 | const isThisLastMessage = messages.length - 1 === messageIdx;
334 |
335 | const isUserMessage = currentMessage.role === "user";
336 |
337 | const reasoning = currentMessage.parts.find(
338 | (part) => part.type === "reasoning"
339 | );
340 |
341 | return (
342 |
349 | {isUserMessage ? (
350 | <>
351 | {currentMessage.content.startsWith(
352 | "The following error occurred when running the code you provided:"
353 | ) ? (
354 |
355 | ) : (
356 |
362 |
363 | {currentMessage.content}
364 |
365 |
366 | )}
367 | >
368 | ) : (
369 |
370 | 0
375 | }
376 | />
377 |
378 |
379 |
383 |
384 |
385 | {currentMessage.isThinking && }
386 |
387 | {currentMessage.toolCall?.toolInvocation.state ===
388 | "result" && (
389 |
390 | {errorCode ? (
391 |
392 | ) : (
393 | <>
394 | {stdOut && }
395 |
396 | {imagePngBase64 && (
397 |
400 | )}
401 | >
402 | )}
403 |
404 | )}
405 | {/* Timestamp for assistant messages */}
406 | {currentMessage.role === "assistant" &&
407 | currentMessage.createdAt && (
408 |
409 |
410 | {typeof currentMessage.duration === "number" && (
411 | <>
412 |
413 | {currentMessage.duration.toFixed(2)}s -
414 |
415 | >
416 | )}
417 | {formatLLMTimestamp(currentMessage.createdAt)}{" "}
418 | {currentMessage?.model && (
419 | <>
420 |
421 | -{" "}
422 | {
423 | CHAT_MODELS.find(
424 | (model) =>
425 | model.model === currentMessage.model
426 | )?.title
427 | }
428 |
429 | >
430 | )}
431 |
432 |
433 | )}
434 |
435 | )}
436 |
437 | {isThisLastMessage && status === "submitted" && (
438 |
439 | )}
440 |
441 | );
442 | })}
443 |
444 |
445 |
446 | setInputValue(value)}
449 | onSend={async () => {
450 | // Clear input and localStorage immediately on submit
451 | const newMessage = inputValue;
452 | clearInputValue();
453 | await append({
454 | role: "user",
455 | content: newMessage,
456 | });
457 | }}
458 | uploadedFile={
459 | uploadedFile && {
460 | url: uploadedFile.url,
461 | csvHeaders: uploadedFile.csvHeaders,
462 | csvRows: uploadedFile.csvRows,
463 | }
464 | }
465 | onStopLLM={() => {
466 | if (status === "submitted" || status === "streaming") {
467 | return stop();
468 | }
469 | if (isCodeRunning && codeAbortController.current) {
470 | codeAbortController.current.abort();
471 | console.log("Aborted code execution frontend");
472 | setIsCodeRunning(false);
473 | }
474 | }}
475 | isLLMAnswering={
476 | status === "submitted" || status === "streaming" || isCodeRunning
477 | }
478 | />
479 |
480 |
481 | );
482 | }
483 |
--------------------------------------------------------------------------------
|