@tellet/create 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +195 -0
  2. package/dist/ai/generate.d.ts +33 -0
  3. package/dist/ai/generate.js +108 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +337 -0
  6. package/dist/scaffold/project.d.ts +44 -0
  7. package/dist/scaffold/project.js +318 -0
  8. package/package.json +48 -0
  9. package/template/Dockerfile +35 -0
  10. package/template/app/(dashboard)/agents/page.tsx +14 -0
  11. package/template/app/(dashboard)/conversations/[id]/page.tsx +103 -0
  12. package/template/app/(dashboard)/conversations/page.tsx +50 -0
  13. package/template/app/(dashboard)/dashboard/page.tsx +102 -0
  14. package/template/app/(dashboard)/layout.tsx +15 -0
  15. package/template/app/(dashboard)/settings/page.tsx +46 -0
  16. package/template/app/(site)/layout.tsx +3 -0
  17. package/template/app/(site)/page.tsx +25 -0
  18. package/template/app/api/chat/route.ts +129 -0
  19. package/template/app/api/cron/route.ts +29 -0
  20. package/template/app/api/orchestrator/route.ts +139 -0
  21. package/template/app/globals.css +30 -0
  22. package/template/app/layout.tsx +18 -0
  23. package/template/components/chat/ChatWidget.tsx +109 -0
  24. package/template/components/chat/Markdown.tsx +136 -0
  25. package/template/components/dashboard/AgentChat.tsx +192 -0
  26. package/template/components/dashboard/AgentsListClient.tsx +86 -0
  27. package/template/components/dashboard/DashboardAgentGrid.tsx +73 -0
  28. package/template/components/dashboard/OrchestratorChat.tsx +251 -0
  29. package/template/components/dashboard/Sidebar.tsx +44 -0
  30. package/template/components/dashboard/StatsCards.tsx +40 -0
  31. package/template/components/dashboard/Welcome.tsx +139 -0
  32. package/template/components/sections/Agents.tsx +67 -0
  33. package/template/components/sections/CTA.tsx +46 -0
  34. package/template/components/sections/FAQ.tsx +81 -0
  35. package/template/components/sections/Features.tsx +51 -0
  36. package/template/components/sections/Footer.tsx +22 -0
  37. package/template/components/sections/Hero.tsx +86 -0
  38. package/template/components/sections/Icons.tsx +29 -0
  39. package/template/components/ui/Button.tsx +26 -0
  40. package/template/docker-compose.yml +32 -0
  41. package/template/infra/bin/app.ts +16 -0
  42. package/template/infra/cdk.json +6 -0
  43. package/template/infra/lib/tellet-stack.ts +216 -0
  44. package/template/infra/package.json +20 -0
  45. package/template/infra/tsconfig.json +16 -0
  46. package/template/lib/db.ts +37 -0
  47. package/template/lib/engine/default.ts +227 -0
  48. package/template/lib/engine/index.ts +17 -0
  49. package/template/lib/mcp/client.ts +97 -0
  50. package/template/lib/mcp/knowledge.ts +84 -0
  51. package/template/lib/mcp/registry.ts +106 -0
  52. package/template/lib/orchestrator/executor.ts +202 -0
  53. package/template/lib/orchestrator/tools.ts +245 -0
  54. package/template/lib/providers/anthropic.ts +41 -0
  55. package/template/lib/providers/index.ts +36 -0
  56. package/template/lib/providers/openai.ts +46 -0
  57. package/template/lib/scheduler.ts +115 -0
  58. package/template/lib/supabase.ts +30 -0
  59. package/template/lib/tellet.ts +45 -0
  60. package/template/lib/utils.ts +6 -0
  61. package/template/next.config.ts +7 -0
  62. package/template/public/widget.js +172 -0
  63. package/template/railway.toml +9 -0
  64. package/template/tsconfig.json +21 -0
@@ -0,0 +1,102 @@
1
+ import { createServerSupabase } from "@/lib/supabase";
2
+ import { Welcome } from "@/components/dashboard/Welcome";
3
+ import { StatsCards } from "@/components/dashboard/StatsCards";
4
+ import { DashboardAgentGrid } from "@/components/dashboard/DashboardAgentGrid";
5
+ import config from "../../../tellet.json";
6
+
7
+ export default async function DashboardPage() {
8
+ const supabase = await createServerSupabase();
9
+
10
+ const [
11
+ { data: agents },
12
+ { count: conversationCount },
13
+ { count: messageCount },
14
+ { data: activity },
15
+ { data: costData },
16
+ ] = await Promise.all([
17
+ supabase.from("agents").select("*").order("created_at"),
18
+ supabase.from("conversations").select("*", { count: "exact", head: true }),
19
+ supabase.from("messages").select("*", { count: "exact", head: true }),
20
+ supabase
21
+ .from("activity_log")
22
+ .select("*, agents(name, role)")
23
+ .order("created_at", { ascending: false })
24
+ .limit(10),
25
+ supabase.from("activity_log").select("cost_usd"),
26
+ ]);
27
+
28
+ const activeAgents = (agents || []).filter((a) => a.status === "active").length;
29
+ const totalCost = (costData || []).reduce(
30
+ (sum, r) => sum + Number(r.cost_usd || 0),
31
+ 0
32
+ );
33
+
34
+ return (
35
+ <div className="space-y-8">
36
+ <div>
37
+ <h1 className="text-2xl font-semibold tracking-tight">
38
+ {config.company.name}
39
+ </h1>
40
+ <p className="text-text-secondary text-sm mt-1">Your AI team is ready.</p>
41
+ </div>
42
+
43
+ <Welcome
44
+ agentCount={agents?.length || 0}
45
+ conversationCount={conversationCount || 0}
46
+ />
47
+
48
+ <StatsCards
49
+ totalConversations={conversationCount || 0}
50
+ totalMessages={messageCount || 0}
51
+ activeAgents={activeAgents}
52
+ estimatedCost={totalCost}
53
+ />
54
+
55
+ <div>
56
+ <h2 className="text-lg font-semibold mb-4">Your Agents</h2>
57
+ <DashboardAgentGrid
58
+ agents={(agents || []).map((a) => ({
59
+ id: a.id,
60
+ name: a.name,
61
+ role: a.role,
62
+ status: a.status,
63
+ }))}
64
+ />
65
+ </div>
66
+
67
+ <div>
68
+ <h2 className="text-lg font-semibold mb-4">Recent Activity</h2>
69
+ {activity && activity.length > 0 ? (
70
+ <div className="space-y-2">
71
+ {activity.map((a) => (
72
+ <div
73
+ key={a.id}
74
+ className="flex items-start gap-3 rounded-lg border border-border bg-bg-secondary/30 px-4 py-3"
75
+ >
76
+ <div className="flex-1 min-w-0">
77
+ <p className="text-sm">
78
+ <span className="font-medium">
79
+ {(a.agents as { name: string })?.name}
80
+ </span>{" "}
81
+ <span className="text-text-secondary">
82
+ {a.summary || a.action}
83
+ </span>
84
+ </p>
85
+ <p className="text-xs text-text-tertiary mt-0.5">
86
+ {new Date(a.created_at).toLocaleString()}
87
+ </p>
88
+ </div>
89
+ </div>
90
+ ))}
91
+ </div>
92
+ ) : (
93
+ <div className="rounded-lg border border-dashed border-border bg-bg-secondary/20 p-8 text-center">
94
+ <p className="text-text-secondary text-sm">
95
+ No activity yet. Chat with your agents to get started.
96
+ </p>
97
+ </div>
98
+ )}
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,15 @@
1
+ import { Sidebar } from "@/components/dashboard/Sidebar";
2
+ import { OrchestratorChat } from "@/components/dashboard/OrchestratorChat";
3
+ import config from "../../tellet.json";
4
+
5
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <div className="flex h-screen overflow-hidden">
8
+ <Sidebar companyName={config.company.name} />
9
+ <main className="flex-1 overflow-y-auto bg-bg-primary">
10
+ <div className="p-8">{children}</div>
11
+ </main>
12
+ <OrchestratorChat />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,46 @@
1
+ import config from "../../../tellet.json";
2
+
3
+ export default function SettingsPage() {
4
+ return (
5
+ <div className="space-y-8 max-w-2xl">
6
+ <h1 className="text-2xl font-semibold tracking-tight">Settings</h1>
7
+
8
+ <div className="rounded-xl border border-border bg-bg-secondary/50 p-6 space-y-3">
9
+ <h2 className="text-lg font-semibold">Company</h2>
10
+ <div className="grid gap-2">
11
+ <div><span className="text-sm text-text-secondary">Name:</span> <span className="text-sm font-medium ml-2">{config.company.name}</span></div>
12
+ <div><span className="text-sm text-text-secondary">Industry:</span> <span className="text-sm font-medium ml-2">{config.company.industry}</span></div>
13
+ </div>
14
+ </div>
15
+
16
+ <div className="rounded-xl border border-border bg-bg-secondary/50 p-6 space-y-3">
17
+ <h2 className="text-lg font-semibold">Infrastructure</h2>
18
+ <div className="grid gap-2">
19
+ <div className="flex items-center justify-between rounded-lg border border-border px-4 py-3">
20
+ <div><p className="text-sm font-medium">Engine</p><p className="text-xs text-text-tertiary">{config.engine}</p></div>
21
+ </div>
22
+ <div className="flex items-center justify-between rounded-lg border border-border px-4 py-3">
23
+ <div><p className="text-sm font-medium">LLM Provider</p><p className="text-xs text-text-tertiary">{config.llm.provider} / {config.llm.defaultModel}</p></div>
24
+ </div>
25
+ <div className="flex items-center justify-between rounded-lg border border-border px-4 py-3">
26
+ <div><p className="text-sm font-medium">Storage</p><p className="text-xs text-text-tertiary">{config.storage}</p></div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+
31
+ <div className="rounded-xl border border-border bg-bg-secondary/50 p-6 space-y-3">
32
+ <h2 className="text-lg font-semibold">Channels</h2>
33
+ <div className="grid gap-2">
34
+ {Object.entries(config.channels).map(([name, ch]) => (
35
+ <div key={name} className="flex items-center justify-between rounded-lg border border-border px-4 py-3">
36
+ <span className="text-sm font-medium capitalize">{name.replace("_", " ")}</span>
37
+ <span className={`text-xs ${ch.enabled ? "text-green-400" : "text-text-tertiary"}`}>
38
+ {ch.enabled ? "Active" : "Inactive"}
39
+ </span>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,3 @@
1
+ export default function SiteLayout({ children }: { children: React.ReactNode }) {
2
+ return <>{children}</>;
3
+ }
@@ -0,0 +1,25 @@
1
+ import { Hero } from "@/components/sections/Hero";
2
+ import { Features } from "@/components/sections/Features";
3
+ import { Agents } from "@/components/sections/Agents";
4
+ import { FAQ } from "@/components/sections/FAQ";
5
+ import { CTA } from "@/components/sections/CTA";
6
+ import { Footer } from "@/components/sections/Footer";
7
+ import { ChatWidget } from "@/components/chat/ChatWidget";
8
+ import config from "../../tellet.json";
9
+
10
+ export default function HomePage() {
11
+ const csAgent =
12
+ config.agents.find((a) => a.role === "customer_support") || config.agents[0];
13
+
14
+ return (
15
+ <>
16
+ <Hero />
17
+ <Features />
18
+ <Agents />
19
+ <FAQ />
20
+ <CTA />
21
+ <Footer />
22
+ <ChatWidget agentId={csAgent.id} agentName={csAgent.name} />
23
+ </>
24
+ );
25
+ }
@@ -0,0 +1,129 @@
1
+ import { createServerSupabase } from "@/lib/supabase";
2
+ import { streamAgentWithTools } from "@/lib/engine";
3
+ import { searchKnowledge } from "@/lib/mcp/knowledge";
4
+ import type Anthropic from "@anthropic-ai/sdk";
5
+
6
+ export async function POST(request: Request) {
7
+ const { message, agent_id, conversation_id } = await request.json();
8
+
9
+ if (!message || !agent_id) {
10
+ return Response.json({ error: "message and agent_id required" }, { status: 400 });
11
+ }
12
+
13
+ const supabase = await createServerSupabase();
14
+
15
+ // Get agent
16
+ const { data: agent } = await supabase
17
+ .from("agents")
18
+ .select("*")
19
+ .eq("id", agent_id)
20
+ .single();
21
+
22
+ if (!agent) return Response.json({ error: "Agent not found" }, { status: 404 });
23
+
24
+ // Get or create conversation
25
+ let convId = conversation_id;
26
+ if (!convId) {
27
+ const { data: conv } = await supabase
28
+ .from("conversations")
29
+ .insert({ agent_id, channel: "web_chat" })
30
+ .select("id")
31
+ .single();
32
+ convId = conv?.id;
33
+ }
34
+
35
+ // Save user message
36
+ await supabase.from("messages").insert({
37
+ conversation_id: convId,
38
+ role: "user",
39
+ content: message,
40
+ });
41
+
42
+ // Get history
43
+ const { data: history } = await supabase
44
+ .from("messages")
45
+ .select("role, content")
46
+ .eq("conversation_id", convId)
47
+ .order("created_at")
48
+ .limit(20);
49
+
50
+ const messages = (history || []).map((m) => ({
51
+ role: m.role as "user" | "assistant",
52
+ content: m.content,
53
+ }));
54
+
55
+ // Built-in tools
56
+ const builtinTools = [
57
+ {
58
+ name: "search_knowledge",
59
+ description: "Search the company knowledge base for product info, policies, and FAQ",
60
+ input_schema: {
61
+ type: "object" as const,
62
+ properties: {
63
+ query: { type: "string", description: "Search query" },
64
+ },
65
+ required: ["query"],
66
+ },
67
+ execute: async (input: Record<string, unknown>) => {
68
+ return searchKnowledge(input.query as string);
69
+ },
70
+ },
71
+ ];
72
+
73
+ // Stream with tool use
74
+ const stream = await streamAgentWithTools({
75
+ agent: {
76
+ id: agent.id,
77
+ name: agent.name,
78
+ role: agent.role,
79
+ model: agent.model,
80
+ provider: agent.config?.provider as string | undefined,
81
+ systemPrompt: agent.system_prompt,
82
+ channels: ["web_chat"],
83
+ tools: agent.config?.tools || [],
84
+ },
85
+ messages,
86
+ builtinTools,
87
+ });
88
+
89
+ let fullResponse = "";
90
+ const encoder = new TextEncoder();
91
+
92
+ const readable = new ReadableStream({
93
+ async start(controller) {
94
+ try {
95
+ const reader = stream.getReader();
96
+ while (true) {
97
+ const { done, value } = await reader.read();
98
+ if (done) break;
99
+ fullResponse += value.text;
100
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text: value.text })}\n\n`));
101
+ }
102
+
103
+ // Save response
104
+ await supabase.from("messages").insert({
105
+ conversation_id: convId,
106
+ role: "assistant",
107
+ content: fullResponse,
108
+ });
109
+
110
+ // Log activity
111
+ await supabase.from("activity_log").insert({
112
+ agent_id,
113
+ action: "replied",
114
+ summary: `Replied: "${fullResponse.slice(0, 80)}${fullResponse.length > 80 ? "..." : ""}"`,
115
+ });
116
+
117
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true, conversation_id: convId })}\n\n`));
118
+ controller.close();
119
+ } catch {
120
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: "Stream failed" })}\n\n`));
121
+ controller.close();
122
+ }
123
+ },
124
+ });
125
+
126
+ return new Response(readable, {
127
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
128
+ });
129
+ }
@@ -0,0 +1,29 @@
1
+ import { runAllScheduledAgents, runScheduledAgent } from "@/lib/scheduler";
2
+
3
+ export async function GET(request: Request) {
4
+ // Verify cron secret (optional, for security)
5
+ const url = new URL(request.url);
6
+ const secret = url.searchParams.get("secret");
7
+ if (process.env.CRON_SECRET && secret !== process.env.CRON_SECRET) {
8
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
9
+ }
10
+
11
+ // Run specific agent or all scheduled agents
12
+ const agentId = url.searchParams.get("agent");
13
+
14
+ try {
15
+ if (agentId) {
16
+ const task = url.searchParams.get("task") || undefined;
17
+ const result = await runScheduledAgent(agentId, task);
18
+ return Response.json(result);
19
+ }
20
+
21
+ const results = await runAllScheduledAgents();
22
+ return Response.json({ results, count: results.length });
23
+ } catch (err) {
24
+ return Response.json(
25
+ { error: err instanceof Error ? err.message : "Cron failed" },
26
+ { status: 500 }
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,139 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { orchestratorTools } from "@/lib/orchestrator/tools";
3
+ import { executeTool } from "@/lib/orchestrator/executor";
4
+ import { getConfig } from "@/lib/tellet";
5
+
6
+ let _client: Anthropic | null = null;
7
+ function getClient() {
8
+ if (!_client) _client = new Anthropic();
9
+ return _client;
10
+ }
11
+
12
+ function buildSystemPrompt(): string {
13
+ const config = getConfig();
14
+ return `You are the Orchestrator for "${config.company.name}", an AI-powered ${config.company.industry} company.
15
+
16
+ Your role is to help the Owner manage and operate their company through conversation. You can:
17
+ - View and manage AI agents (list, update prompts)
18
+ - Check company statistics (conversations, messages, costs)
19
+ - Update the website content (tagline, features, FAQ)
20
+ - View recent conversations
21
+
22
+ Company: ${config.company.name}
23
+ Industry: ${config.company.industry}
24
+ Description: ${config.company.description}
25
+ Agents: ${config.agents.map((a) => `${a.name} (${a.role})`).join(", ")}
26
+
27
+ Guidelines:
28
+ - Be helpful and proactive. Suggest improvements when you see opportunities.
29
+ - When updating content, explain what you changed and why.
30
+ - For potentially impactful changes, confirm with the owner before executing.
31
+ - Keep responses concise and actionable.
32
+ - Speak in the language the owner uses.`;
33
+ }
34
+
35
+ export async function POST(request: Request) {
36
+ const { messages } = await request.json();
37
+
38
+ if (!messages || !Array.isArray(messages)) {
39
+ return Response.json({ error: "messages array required" }, { status: 400 });
40
+ }
41
+
42
+ const encoder = new TextEncoder();
43
+
44
+ const readable = new ReadableStream({
45
+ async start(controller) {
46
+ try {
47
+ let currentMessages: Anthropic.MessageParam[] = messages.map(
48
+ (m: { role: string; content: string }) => ({
49
+ role: m.role as "user" | "assistant",
50
+ content: m.content,
51
+ })
52
+ );
53
+
54
+ // Agentic loop — keep running until no more tool calls
55
+ while (true) {
56
+ const response = await getClient().messages.create({
57
+ model: "claude-sonnet-4-6",
58
+ max_tokens: 4096,
59
+ system: buildSystemPrompt(),
60
+ tools: orchestratorTools,
61
+ messages: currentMessages,
62
+ });
63
+
64
+ // Collect text and tool use blocks
65
+ let hasToolUse = false;
66
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
67
+
68
+ for (const block of response.content) {
69
+ if (block.type === "text" && block.text) {
70
+ controller.enqueue(
71
+ encoder.encode(`data: ${JSON.stringify({ text: block.text })}\n\n`)
72
+ );
73
+ }
74
+
75
+ if (block.type === "tool_use") {
76
+ hasToolUse = true;
77
+
78
+ // Notify client which tool is running
79
+ controller.enqueue(
80
+ encoder.encode(
81
+ `data: ${JSON.stringify({ tool: block.name, status: "running" })}\n\n`
82
+ )
83
+ );
84
+
85
+ const result = await executeTool(
86
+ block.name,
87
+ block.input as Record<string, unknown>
88
+ );
89
+
90
+ toolResults.push({
91
+ type: "tool_result",
92
+ tool_use_id: block.id,
93
+ content: result,
94
+ });
95
+
96
+ controller.enqueue(
97
+ encoder.encode(
98
+ `data: ${JSON.stringify({ tool: block.name, status: "done" })}\n\n`
99
+ )
100
+ );
101
+ }
102
+ }
103
+
104
+ if (!hasToolUse) {
105
+ // No more tool calls — we're done
106
+ break;
107
+ }
108
+
109
+ // Add assistant response + tool results, continue loop
110
+ currentMessages = [
111
+ ...currentMessages,
112
+ { role: "assistant" as const, content: response.content },
113
+ { role: "user" as const, content: toolResults },
114
+ ];
115
+ }
116
+
117
+ controller.enqueue(
118
+ encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`)
119
+ );
120
+ controller.close();
121
+ } catch (err) {
122
+ controller.enqueue(
123
+ encoder.encode(
124
+ `data: ${JSON.stringify({ error: err instanceof Error ? err.message : "Orchestrator error" })}\n\n`
125
+ )
126
+ );
127
+ controller.close();
128
+ }
129
+ },
130
+ });
131
+
132
+ return new Response(readable, {
133
+ headers: {
134
+ "Content-Type": "text/event-stream",
135
+ "Cache-Control": "no-cache",
136
+ Connection: "keep-alive",
137
+ },
138
+ });
139
+ }
@@ -0,0 +1,30 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme inline {
4
+ --color-bg-primary: #09090b;
5
+ --color-bg-secondary: #18181b;
6
+ --color-bg-tertiary: #27272a;
7
+ --color-text-primary: #fafafa;
8
+ --color-text-secondary: #a1a1aa;
9
+ --color-text-tertiary: #52525b;
10
+ --color-accent: #8b5cf6;
11
+ --color-accent-hover: #a78bfa;
12
+ --color-accent-glow: rgba(139, 92, 246, 0.15);
13
+ --color-highlight: #f59e0b;
14
+ --color-border: #27272a;
15
+ --color-border-hover: #3f3f46;
16
+ --font-sans: var(--font-inter);
17
+ }
18
+
19
+ html { scroll-behavior: smooth; }
20
+
21
+ body {
22
+ background: var(--color-bg-primary);
23
+ color: var(--color-text-primary);
24
+ font-family: var(--font-sans), system-ui, sans-serif;
25
+ }
26
+
27
+ ::selection {
28
+ background: var(--color-accent);
29
+ color: white;
30
+ }
@@ -0,0 +1,18 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const inter = Inter({ variable: "--font-inter", subsets: ["latin"] });
6
+
7
+ export const metadata: Metadata = {
8
+ title: "{{COMPANY_NAME}}",
9
+ description: "Powered by tellet — AI Agentic Company",
10
+ };
11
+
12
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
13
+ return (
14
+ <html lang="en" className={`${inter.variable} h-full antialiased`}>
15
+ <body className="min-h-full flex flex-col">{children}</body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { cn } from "@/lib/utils";
5
+ import { Markdown } from "./Markdown";
6
+
7
+ interface ChatMessage {
8
+ role: "user" | "assistant";
9
+ content: string;
10
+ }
11
+
12
+ export function ChatWidget({ agentId, agentName }: { agentId: string; agentName: string }) {
13
+ const [open, setOpen] = useState(false);
14
+ const [input, setInput] = useState("");
15
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
16
+ const [streaming, setStreaming] = useState(false);
17
+ const [conversationId, setConversationId] = useState<string | null>(null);
18
+ const endRef = useRef<HTMLDivElement>(null);
19
+
20
+ useEffect(() => { endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);
21
+
22
+ const send = async () => {
23
+ const text = input.trim();
24
+ if (!text || streaming) return;
25
+ setInput("");
26
+ setMessages((p) => [...p, { role: "user", content: text }]);
27
+ setStreaming(true);
28
+ setMessages((p) => [...p, { role: "assistant", content: "" }]);
29
+
30
+ try {
31
+ const res = await fetch("/api/chat", {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify({ message: text, agent_id: agentId, conversation_id: conversationId }),
35
+ });
36
+ const reader = res.body?.getReader();
37
+ const decoder = new TextDecoder();
38
+ if (!reader) return;
39
+
40
+ while (true) {
41
+ const { done, value } = await reader.read();
42
+ if (done) break;
43
+ for (const line of decoder.decode(value).split("\n").filter((l) => l.startsWith("data: "))) {
44
+ try {
45
+ const data = JSON.parse(line.slice(6));
46
+ if (data.text) {
47
+ setMessages((p) => {
48
+ const u = [...p];
49
+ const last = u[u.length - 1];
50
+ if (last.role === "assistant") u[u.length - 1] = { ...last, content: last.content + data.text };
51
+ return u;
52
+ });
53
+ }
54
+ if (data.conversation_id) setConversationId(data.conversation_id);
55
+ } catch {}
56
+ }
57
+ }
58
+ } catch {
59
+ setMessages((p) => { const u = [...p]; u[u.length - 1] = { role: "assistant", content: "Something went wrong." }; return u; });
60
+ } finally { setStreaming(false); }
61
+ };
62
+
63
+ return (
64
+ <>
65
+ <button
66
+ id="chat-trigger"
67
+ onClick={() => setOpen(!open)}
68
+ className={cn(
69
+ "fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full flex items-center justify-center shadow-lg transition-all cursor-pointer",
70
+ open ? "bg-bg-secondary border border-border" : "bg-accent hover:bg-accent-hover shadow-[0_0_30px_var(--color-accent-glow)]"
71
+ )}
72
+ >
73
+ {open ? (
74
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
75
+ ) : (
76
+ <svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" /></svg>
77
+ )}
78
+ </button>
79
+
80
+ {open && (
81
+ <div className="fixed bottom-24 right-6 z-50 w-[380px] max-h-[500px] rounded-2xl border border-border bg-bg-primary shadow-2xl flex flex-col overflow-hidden">
82
+ <div className="px-4 py-3 border-b border-border bg-bg-secondary/50 flex items-center gap-2">
83
+ <span className="w-7 h-7 rounded-full bg-accent/20 text-accent text-[10px] font-bold flex items-center justify-center">AI</span>
84
+ <div><p className="text-sm font-semibold">{agentName}</p><p className="text-[11px] text-text-tertiary">Online</p></div>
85
+ </div>
86
+ <div className="flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-[200px] max-h-[340px]">
87
+ {messages.length === 0 && <div className="text-center py-8"><p className="text-sm text-text-secondary">Ask me anything!</p></div>}
88
+ {messages.map((m, i) => (
89
+ <div key={i} className={cn("flex", m.role === "user" ? "justify-end" : "justify-start")}>
90
+ <div className={cn("rounded-xl px-3 py-2 max-w-[85%] text-sm leading-relaxed", m.role === "user" ? "bg-accent text-white" : "bg-bg-secondary text-text-primary border border-border")}>
91
+ {m.content ? <Markdown content={m.content} /> : <span className="inline-flex gap-1"><span className="w-1.5 h-1.5 rounded-full bg-text-tertiary animate-pulse" /><span className="w-1.5 h-1.5 rounded-full bg-text-tertiary animate-pulse [animation-delay:150ms]" /><span className="w-1.5 h-1.5 rounded-full bg-text-tertiary animate-pulse [animation-delay:300ms]" /></span>}
92
+ </div>
93
+ </div>
94
+ ))}
95
+ <div ref={endRef} />
96
+ </div>
97
+ <form onSubmit={(e) => { e.preventDefault(); send(); }} className="px-3 py-3 border-t border-border flex gap-2">
98
+ <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." disabled={streaming}
99
+ className="flex-1 rounded-lg bg-bg-secondary border border-border px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:border-accent disabled:opacity-50" />
100
+ <button type="submit" disabled={streaming || !input.trim()}
101
+ className="rounded-lg bg-accent px-3 py-2 text-white text-sm hover:bg-accent-hover disabled:opacity-50 cursor-pointer transition-colors">
102
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" /></svg>
103
+ </button>
104
+ </form>
105
+ </div>
106
+ )}
107
+ </>
108
+ );
109
+ }