@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.
- package/README.md +195 -0
- package/dist/ai/generate.d.ts +33 -0
- package/dist/ai/generate.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +337 -0
- package/dist/scaffold/project.d.ts +44 -0
- package/dist/scaffold/project.js +318 -0
- package/package.json +48 -0
- package/template/Dockerfile +35 -0
- package/template/app/(dashboard)/agents/page.tsx +14 -0
- package/template/app/(dashboard)/conversations/[id]/page.tsx +103 -0
- package/template/app/(dashboard)/conversations/page.tsx +50 -0
- package/template/app/(dashboard)/dashboard/page.tsx +102 -0
- package/template/app/(dashboard)/layout.tsx +15 -0
- package/template/app/(dashboard)/settings/page.tsx +46 -0
- package/template/app/(site)/layout.tsx +3 -0
- package/template/app/(site)/page.tsx +25 -0
- package/template/app/api/chat/route.ts +129 -0
- package/template/app/api/cron/route.ts +29 -0
- package/template/app/api/orchestrator/route.ts +139 -0
- package/template/app/globals.css +30 -0
- package/template/app/layout.tsx +18 -0
- package/template/components/chat/ChatWidget.tsx +109 -0
- package/template/components/chat/Markdown.tsx +136 -0
- package/template/components/dashboard/AgentChat.tsx +192 -0
- package/template/components/dashboard/AgentsListClient.tsx +86 -0
- package/template/components/dashboard/DashboardAgentGrid.tsx +73 -0
- package/template/components/dashboard/OrchestratorChat.tsx +251 -0
- package/template/components/dashboard/Sidebar.tsx +44 -0
- package/template/components/dashboard/StatsCards.tsx +40 -0
- package/template/components/dashboard/Welcome.tsx +139 -0
- package/template/components/sections/Agents.tsx +67 -0
- package/template/components/sections/CTA.tsx +46 -0
- package/template/components/sections/FAQ.tsx +81 -0
- package/template/components/sections/Features.tsx +51 -0
- package/template/components/sections/Footer.tsx +22 -0
- package/template/components/sections/Hero.tsx +86 -0
- package/template/components/sections/Icons.tsx +29 -0
- package/template/components/ui/Button.tsx +26 -0
- package/template/docker-compose.yml +32 -0
- package/template/infra/bin/app.ts +16 -0
- package/template/infra/cdk.json +6 -0
- package/template/infra/lib/tellet-stack.ts +216 -0
- package/template/infra/package.json +20 -0
- package/template/infra/tsconfig.json +16 -0
- package/template/lib/db.ts +37 -0
- package/template/lib/engine/default.ts +227 -0
- package/template/lib/engine/index.ts +17 -0
- package/template/lib/mcp/client.ts +97 -0
- package/template/lib/mcp/knowledge.ts +84 -0
- package/template/lib/mcp/registry.ts +106 -0
- package/template/lib/orchestrator/executor.ts +202 -0
- package/template/lib/orchestrator/tools.ts +245 -0
- package/template/lib/providers/anthropic.ts +41 -0
- package/template/lib/providers/index.ts +36 -0
- package/template/lib/providers/openai.ts +46 -0
- package/template/lib/scheduler.ts +115 -0
- package/template/lib/supabase.ts +30 -0
- package/template/lib/tellet.ts +45 -0
- package/template/lib/utils.ts +6 -0
- package/template/next.config.ts +7 -0
- package/template/public/widget.js +172 -0
- package/template/railway.toml +9 -0
- 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,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
|
+
}
|