@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,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Markdown } from "@/components/chat/Markdown";
|
|
7
|
+
|
|
8
|
+
interface ChatMessage {
|
|
9
|
+
role: "user" | "assistant";
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function OrchestratorChat() {
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const [input, setInput] = useState("");
|
|
16
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
17
|
+
const [streaming, setStreaming] = useState(false);
|
|
18
|
+
const [toolStatus, setToolStatus] = useState<string | null>(null);
|
|
19
|
+
const endRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
23
|
+
}, [messages, toolStatus]);
|
|
24
|
+
|
|
25
|
+
const send = async () => {
|
|
26
|
+
const text = input.trim();
|
|
27
|
+
if (!text || streaming) return;
|
|
28
|
+
setInput("");
|
|
29
|
+
|
|
30
|
+
const newMessages = [...messages, { role: "user" as const, content: text }];
|
|
31
|
+
setMessages(newMessages);
|
|
32
|
+
setStreaming(true);
|
|
33
|
+
setMessages((p) => [...p, { role: "assistant", content: "" }]);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch("/api/orchestrator", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
messages: newMessages.map((m) => ({ role: m.role, content: m.content })),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const reader = res.body?.getReader();
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
if (!reader) return;
|
|
47
|
+
|
|
48
|
+
while (true) {
|
|
49
|
+
const { done, value } = await reader.read();
|
|
50
|
+
if (done) break;
|
|
51
|
+
for (const line of decoder
|
|
52
|
+
.decode(value)
|
|
53
|
+
.split("\n")
|
|
54
|
+
.filter((l) => l.startsWith("data: "))) {
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(line.slice(6));
|
|
57
|
+
|
|
58
|
+
if (data.text) {
|
|
59
|
+
setMessages((p) => {
|
|
60
|
+
const u = [...p];
|
|
61
|
+
const last = u[u.length - 1];
|
|
62
|
+
if (last.role === "assistant")
|
|
63
|
+
u[u.length - 1] = { ...last, content: last.content + data.text };
|
|
64
|
+
return u;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (data.tool) {
|
|
69
|
+
setToolStatus(
|
|
70
|
+
data.status === "running"
|
|
71
|
+
? `Running ${data.tool.replace("_", " ")}...`
|
|
72
|
+
: null
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (data.error) {
|
|
77
|
+
setMessages((p) => {
|
|
78
|
+
const u = [...p];
|
|
79
|
+
u[u.length - 1] = { role: "assistant", content: `Error: ${data.error}` };
|
|
80
|
+
return u;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
setMessages((p) => {
|
|
88
|
+
const u = [...p];
|
|
89
|
+
u[u.length - 1] = { role: "assistant", content: "Connection error." };
|
|
90
|
+
return u;
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
setStreaming(false);
|
|
94
|
+
setToolStatus(null);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
{/* Trigger button */}
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => setOpen(!open)}
|
|
103
|
+
className={cn(
|
|
104
|
+
"fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-full px-5 py-3 shadow-lg transition-all cursor-pointer",
|
|
105
|
+
open
|
|
106
|
+
? "bg-bg-secondary border border-border"
|
|
107
|
+
: "bg-accent hover:bg-accent-hover text-white shadow-[0_0_30px_var(--color-accent-glow)]"
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
{open ? (
|
|
111
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
112
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
113
|
+
</svg>
|
|
114
|
+
) : (
|
|
115
|
+
<>
|
|
116
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
117
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09Z" />
|
|
118
|
+
</svg>
|
|
119
|
+
<span className="text-sm font-medium">Orchestrator</span>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
{/* Chat panel */}
|
|
125
|
+
<AnimatePresence>
|
|
126
|
+
{open && (
|
|
127
|
+
<motion.div
|
|
128
|
+
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
129
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
130
|
+
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
131
|
+
transition={{ duration: 0.15 }}
|
|
132
|
+
className="fixed bottom-20 right-6 z-50 w-[420px] max-h-[600px] rounded-2xl border border-border bg-bg-primary shadow-2xl flex flex-col overflow-hidden"
|
|
133
|
+
>
|
|
134
|
+
{/* Header */}
|
|
135
|
+
<div className="px-5 py-4 border-b border-border bg-bg-secondary/50">
|
|
136
|
+
<div className="flex items-center gap-2">
|
|
137
|
+
<div className="w-7 h-7 rounded-full bg-accent/20 text-accent flex items-center justify-center">
|
|
138
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
139
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09Z" />
|
|
140
|
+
</svg>
|
|
141
|
+
</div>
|
|
142
|
+
<div>
|
|
143
|
+
<p className="text-sm font-semibold">Orchestrator</p>
|
|
144
|
+
<p className="text-[11px] text-text-tertiary">
|
|
145
|
+
Manage your company with AI
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Messages */}
|
|
152
|
+
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-[200px] max-h-[420px]">
|
|
153
|
+
{messages.length === 0 && (
|
|
154
|
+
<div className="text-center py-8 space-y-3">
|
|
155
|
+
<p className="text-sm text-text-secondary">
|
|
156
|
+
I can help you manage your AI company.
|
|
157
|
+
</p>
|
|
158
|
+
<div className="flex flex-wrap gap-2 justify-center">
|
|
159
|
+
{[
|
|
160
|
+
"Show my stats",
|
|
161
|
+
"List my agents",
|
|
162
|
+
"Update the website tagline",
|
|
163
|
+
"Recent conversations",
|
|
164
|
+
].map((suggestion) => (
|
|
165
|
+
<button
|
|
166
|
+
key={suggestion}
|
|
167
|
+
onClick={() => {
|
|
168
|
+
setInput(suggestion);
|
|
169
|
+
}}
|
|
170
|
+
className="text-xs bg-bg-secondary border border-border rounded-lg px-3 py-1.5 text-text-secondary hover:text-text-primary hover:border-border-hover transition-colors cursor-pointer"
|
|
171
|
+
>
|
|
172
|
+
{suggestion}
|
|
173
|
+
</button>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{messages.map((m, i) => (
|
|
180
|
+
<div
|
|
181
|
+
key={i}
|
|
182
|
+
className={cn(
|
|
183
|
+
"flex",
|
|
184
|
+
m.role === "user" ? "justify-end" : "justify-start"
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
<div
|
|
188
|
+
className={cn(
|
|
189
|
+
"rounded-xl px-3 py-2 max-w-[85%] text-sm leading-relaxed",
|
|
190
|
+
m.role === "user"
|
|
191
|
+
? "bg-accent text-white"
|
|
192
|
+
: "bg-bg-secondary text-text-primary border border-border"
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
{m.content ? (
|
|
196
|
+
<Markdown content={m.content} />
|
|
197
|
+
) : (
|
|
198
|
+
<span className="inline-flex gap-1">
|
|
199
|
+
<span className="w-1.5 h-1.5 rounded-full bg-text-tertiary animate-pulse" />
|
|
200
|
+
<span className="w-1.5 h-1.5 rounded-full bg-text-tertiary animate-pulse [animation-delay:150ms]" />
|
|
201
|
+
<span className="w-1.5 h-1.5 rounded-full bg-text-tertiary animate-pulse [animation-delay:300ms]" />
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
|
|
208
|
+
{toolStatus && (
|
|
209
|
+
<div className="flex items-center gap-2 text-xs text-accent">
|
|
210
|
+
<svg className="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
211
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
212
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
213
|
+
</svg>
|
|
214
|
+
{toolStatus}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<div ref={endRef} />
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Input */}
|
|
222
|
+
<form
|
|
223
|
+
onSubmit={(e) => {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
send();
|
|
226
|
+
}}
|
|
227
|
+
className="px-3 py-3 border-t border-border flex gap-2"
|
|
228
|
+
>
|
|
229
|
+
<input
|
|
230
|
+
value={input}
|
|
231
|
+
onChange={(e) => setInput(e.target.value)}
|
|
232
|
+
placeholder="Tell me what to do..."
|
|
233
|
+
disabled={streaming}
|
|
234
|
+
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"
|
|
235
|
+
/>
|
|
236
|
+
<button
|
|
237
|
+
type="submit"
|
|
238
|
+
disabled={streaming || !input.trim()}
|
|
239
|
+
className="rounded-lg bg-accent px-3 py-2 text-white text-sm hover:bg-accent-hover disabled:opacity-50 cursor-pointer transition-colors"
|
|
240
|
+
>
|
|
241
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
242
|
+
<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" />
|
|
243
|
+
</svg>
|
|
244
|
+
</button>
|
|
245
|
+
</form>
|
|
246
|
+
</motion.div>
|
|
247
|
+
)}
|
|
248
|
+
</AnimatePresence>
|
|
249
|
+
</>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const nav = [
|
|
8
|
+
{ label: "Dashboard", href: "/dashboard", icon: "H" },
|
|
9
|
+
{ label: "Agents", href: "/agents", icon: "A" },
|
|
10
|
+
{ label: "Conversations", href: "/conversations", icon: "C" },
|
|
11
|
+
{ label: "Settings", href: "/settings", icon: "S" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function Sidebar({ companyName }: { companyName: string }) {
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<aside className="w-56 border-r border-border bg-bg-primary flex flex-col h-full">
|
|
19
|
+
<Link href="/" className="block p-5 hover:bg-bg-secondary/50 transition-colors">
|
|
20
|
+
<span className="text-lg font-bold tracking-tight">{companyName}</span>
|
|
21
|
+
<p className="text-[11px] text-text-tertiary mt-0.5">Powered by tellet</p>
|
|
22
|
+
</Link>
|
|
23
|
+
<nav className="flex-1 px-3 space-y-0.5">
|
|
24
|
+
{nav.map((item) => (
|
|
25
|
+
<Link
|
|
26
|
+
key={item.href}
|
|
27
|
+
href={item.href}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
|
|
30
|
+
pathname === item.href
|
|
31
|
+
? "bg-accent/10 text-accent"
|
|
32
|
+
: "text-text-secondary hover:text-text-primary hover:bg-bg-secondary"
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<span className="w-5 h-5 rounded bg-bg-tertiary flex items-center justify-center text-[10px] font-bold">
|
|
36
|
+
{item.icon}
|
|
37
|
+
</span>
|
|
38
|
+
{item.label}
|
|
39
|
+
</Link>
|
|
40
|
+
))}
|
|
41
|
+
</nav>
|
|
42
|
+
</aside>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
interface StatsCardsProps {
|
|
2
|
+
totalConversations: number;
|
|
3
|
+
totalMessages: number;
|
|
4
|
+
activeAgents: number;
|
|
5
|
+
estimatedCost: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const cards = [
|
|
9
|
+
{ key: "conversations", label: "Conversations", color: "text-blue-400" },
|
|
10
|
+
{ key: "messages", label: "Messages", color: "text-green-400" },
|
|
11
|
+
{ key: "agents", label: "Active Agents", color: "text-purple-400" },
|
|
12
|
+
{ key: "cost", label: "Est. Cost", color: "text-amber-400" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export function StatsCards({ totalConversations, totalMessages, activeAgents, estimatedCost }: StatsCardsProps) {
|
|
16
|
+
const values: Record<string, string> = {
|
|
17
|
+
conversations: String(totalConversations),
|
|
18
|
+
messages: String(totalMessages),
|
|
19
|
+
agents: String(activeAgents),
|
|
20
|
+
cost: `$${estimatedCost.toFixed(2)}`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
25
|
+
{cards.map((card) => (
|
|
26
|
+
<div
|
|
27
|
+
key={card.key}
|
|
28
|
+
className="rounded-xl border border-border bg-bg-secondary/50 p-5"
|
|
29
|
+
>
|
|
30
|
+
<p className="text-xs text-text-tertiary uppercase tracking-wider mb-2">
|
|
31
|
+
{card.label}
|
|
32
|
+
</p>
|
|
33
|
+
<p className={`text-2xl font-semibold ${card.color}`}>
|
|
34
|
+
{values[card.key]}
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY = "tellet_onboarding_dismissed";
|
|
7
|
+
|
|
8
|
+
interface WelcomeProps {
|
|
9
|
+
agentCount: number;
|
|
10
|
+
conversationCount: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const steps = [
|
|
14
|
+
{ id: "agents", label: "AI team created", auto: true },
|
|
15
|
+
{ id: "migration", label: "Database migration", auto: true },
|
|
16
|
+
{ id: "conversation", label: "First conversation", auto: false },
|
|
17
|
+
{ id: "deploy", label: "Deploy to production", auto: false },
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export function Welcome({ agentCount, conversationCount }: WelcomeProps) {
|
|
21
|
+
const [dismissed, setDismissed] = useState(true);
|
|
22
|
+
const [deployChecked, setDeployChecked] = useState(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setDismissed(localStorage.getItem(STORAGE_KEY) === "true");
|
|
26
|
+
setDeployChecked(localStorage.getItem("tellet_deploy_done") === "true");
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (dismissed) return null;
|
|
30
|
+
|
|
31
|
+
const checks = {
|
|
32
|
+
agents: agentCount > 0,
|
|
33
|
+
migration: agentCount > 0,
|
|
34
|
+
conversation: conversationCount > 0,
|
|
35
|
+
deploy: deployChecked,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const completed = Object.values(checks).filter(Boolean).length;
|
|
39
|
+
const total = steps.length;
|
|
40
|
+
|
|
41
|
+
const dismiss = () => {
|
|
42
|
+
localStorage.setItem(STORAGE_KEY, "true");
|
|
43
|
+
setDismissed(true);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const toggleDeploy = () => {
|
|
47
|
+
const next = !deployChecked;
|
|
48
|
+
setDeployChecked(next);
|
|
49
|
+
localStorage.setItem("tellet_deploy_done", String(next));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<AnimatePresence>
|
|
54
|
+
<motion.div
|
|
55
|
+
initial={{ opacity: 0, y: -10 }}
|
|
56
|
+
animate={{ opacity: 1, y: 0 }}
|
|
57
|
+
exit={{ opacity: 0, height: 0 }}
|
|
58
|
+
className="rounded-xl border border-accent/20 bg-accent/5 p-6 mb-8"
|
|
59
|
+
>
|
|
60
|
+
<div className="flex items-start justify-between mb-4">
|
|
61
|
+
<div>
|
|
62
|
+
<h3 className="text-lg font-semibold">Welcome to your AI company</h3>
|
|
63
|
+
<p className="text-sm text-text-secondary mt-1">
|
|
64
|
+
{completed}/{total} steps complete
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<button
|
|
68
|
+
onClick={dismiss}
|
|
69
|
+
className="text-text-tertiary hover:text-text-secondary transition-colors cursor-pointer"
|
|
70
|
+
>
|
|
71
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
72
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="w-full bg-bg-tertiary rounded-full h-1.5 mb-5">
|
|
78
|
+
<div
|
|
79
|
+
className="bg-accent h-1.5 rounded-full transition-all duration-500"
|
|
80
|
+
style={{ width: `${(completed / total) * 100}%` }}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="space-y-3">
|
|
85
|
+
{steps.map((step) => {
|
|
86
|
+
const checked = checks[step.id];
|
|
87
|
+
return (
|
|
88
|
+
<div key={step.id} className="flex items-center gap-3">
|
|
89
|
+
{step.id === "deploy" ? (
|
|
90
|
+
<button
|
|
91
|
+
onClick={toggleDeploy}
|
|
92
|
+
className={`w-5 h-5 rounded border flex items-center justify-center flex-shrink-0 cursor-pointer transition-colors ${
|
|
93
|
+
checked
|
|
94
|
+
? "bg-accent border-accent"
|
|
95
|
+
: "border-border hover:border-accent/50"
|
|
96
|
+
}`}
|
|
97
|
+
>
|
|
98
|
+
{checked && (
|
|
99
|
+
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
100
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
|
101
|
+
</svg>
|
|
102
|
+
)}
|
|
103
|
+
</button>
|
|
104
|
+
) : (
|
|
105
|
+
<div
|
|
106
|
+
className={`w-5 h-5 rounded border flex items-center justify-center flex-shrink-0 ${
|
|
107
|
+
checked
|
|
108
|
+
? "bg-accent border-accent"
|
|
109
|
+
: "border-border"
|
|
110
|
+
}`}
|
|
111
|
+
>
|
|
112
|
+
{checked && (
|
|
113
|
+
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
114
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
|
115
|
+
</svg>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
<span className={`text-sm ${checked ? "text-text-tertiary line-through" : "text-text-primary"}`}>
|
|
120
|
+
{step.label}
|
|
121
|
+
</span>
|
|
122
|
+
{step.id === "migration" && !checked && (
|
|
123
|
+
<code className="text-xs bg-bg-tertiary px-2 py-0.5 rounded text-text-secondary ml-auto">
|
|
124
|
+
npx supabase db push
|
|
125
|
+
</code>
|
|
126
|
+
)}
|
|
127
|
+
{step.id === "deploy" && !checked && (
|
|
128
|
+
<code className="text-xs bg-bg-tertiary px-2 py-0.5 rounded text-text-secondary ml-auto">
|
|
129
|
+
vercel deploy
|
|
130
|
+
</code>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
</motion.div>
|
|
137
|
+
</AnimatePresence>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
import config from "../../tellet.json";
|
|
5
|
+
|
|
6
|
+
const roleColors: Record<string, string> = {
|
|
7
|
+
customer_support: "bg-green-500/10 text-green-400 border-green-500/20",
|
|
8
|
+
marketing: "bg-blue-500/10 text-blue-400 border-blue-500/20",
|
|
9
|
+
sales: "bg-amber-500/10 text-amber-400 border-amber-500/20",
|
|
10
|
+
operations: "bg-purple-500/10 text-purple-400 border-purple-500/20",
|
|
11
|
+
development: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20",
|
|
12
|
+
analytics: "bg-rose-500/10 text-rose-400 border-rose-500/20",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function Agents() {
|
|
16
|
+
return (
|
|
17
|
+
<section className="py-24 px-6 bg-bg-secondary/30">
|
|
18
|
+
<div className="max-w-5xl mx-auto">
|
|
19
|
+
<motion.div
|
|
20
|
+
className="text-center mb-16"
|
|
21
|
+
initial={{ opacity: 0, y: 20 }}
|
|
22
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
23
|
+
viewport={{ once: true }}
|
|
24
|
+
transition={{ duration: 0.5 }}
|
|
25
|
+
>
|
|
26
|
+
<h2 className="text-3xl md:text-4xl font-bold tracking-tight">
|
|
27
|
+
Meet our AI team
|
|
28
|
+
</h2>
|
|
29
|
+
<p className="mt-3 text-text-secondary">
|
|
30
|
+
Always online. Always ready to help.
|
|
31
|
+
</p>
|
|
32
|
+
</motion.div>
|
|
33
|
+
|
|
34
|
+
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
|
35
|
+
{config.agents.map((agent, i) => (
|
|
36
|
+
<motion.div
|
|
37
|
+
key={agent.id}
|
|
38
|
+
className="rounded-xl border border-border bg-bg-primary p-6 transition-all duration-200 hover:border-border-hover"
|
|
39
|
+
initial={{ opacity: 0, y: 20 }}
|
|
40
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
41
|
+
viewport={{ once: true }}
|
|
42
|
+
transition={{ delay: i * 0.08, duration: 0.4 }}
|
|
43
|
+
>
|
|
44
|
+
<div className="flex items-center gap-3 mb-4">
|
|
45
|
+
<div className="w-10 h-10 rounded-full bg-accent/10 text-accent text-sm font-bold flex items-center justify-center">
|
|
46
|
+
{agent.name[0]}
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<p className="font-semibold text-text-primary">{agent.name}</p>
|
|
50
|
+
<span
|
|
51
|
+
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${roleColors[agent.role] || "bg-bg-tertiary text-text-secondary border-border"}`}
|
|
52
|
+
>
|
|
53
|
+
{agent.role.replace("_", " ")}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
|
59
|
+
<span className="text-xs text-text-secondary">Online</span>
|
|
60
|
+
</div>
|
|
61
|
+
</motion.div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
import config from "../../tellet.json";
|
|
5
|
+
|
|
6
|
+
export function CTA() {
|
|
7
|
+
return (
|
|
8
|
+
<section className="py-24 px-6">
|
|
9
|
+
<motion.div
|
|
10
|
+
className="max-w-3xl mx-auto text-center rounded-2xl border border-border bg-bg-secondary/50 p-12 md:p-16 relative overflow-hidden"
|
|
11
|
+
initial={{ opacity: 0, y: 20 }}
|
|
12
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
13
|
+
viewport={{ once: true }}
|
|
14
|
+
transition={{ duration: 0.5 }}
|
|
15
|
+
>
|
|
16
|
+
<div
|
|
17
|
+
className="absolute inset-0 pointer-events-none"
|
|
18
|
+
style={{
|
|
19
|
+
background:
|
|
20
|
+
"radial-gradient(ellipse 60% 50% at 50% 50%, rgba(139,92,246,0.06) 0%, transparent 70%)",
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
<div className="relative">
|
|
24
|
+
<h2 className="text-3xl md:text-4xl font-bold tracking-tight mb-4">
|
|
25
|
+
{config.site.cta || "Ready to get started?"}
|
|
26
|
+
</h2>
|
|
27
|
+
<p className="text-text-secondary mb-8 max-w-lg mx-auto">
|
|
28
|
+
Our AI team is available 24/7 to help you. Start a conversation now.
|
|
29
|
+
</p>
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => {
|
|
32
|
+
const el = document.getElementById("chat-trigger");
|
|
33
|
+
el?.click();
|
|
34
|
+
}}
|
|
35
|
+
className="inline-flex items-center gap-2 rounded-xl bg-accent px-8 py-4 text-sm font-medium text-white hover:bg-accent-hover transition-all shadow-[0_0_30px_var(--color-accent-glow)] cursor-pointer"
|
|
36
|
+
>
|
|
37
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
38
|
+
<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" />
|
|
39
|
+
</svg>
|
|
40
|
+
Chat with us
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</motion.div>
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import config from "../../tellet.json";
|
|
6
|
+
|
|
7
|
+
function FAQItem({ question, answer }: { question: string; answer: string }) {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="border-b border-border">
|
|
12
|
+
<button
|
|
13
|
+
onClick={() => setOpen(!open)}
|
|
14
|
+
className="w-full flex items-center justify-between py-5 text-left cursor-pointer group"
|
|
15
|
+
>
|
|
16
|
+
<span className="text-sm font-medium text-text-primary group-hover:text-accent transition-colors pr-4">
|
|
17
|
+
{question}
|
|
18
|
+
</span>
|
|
19
|
+
<svg
|
|
20
|
+
className={`w-4 h-4 text-text-tertiary flex-shrink-0 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
|
21
|
+
fill="none"
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
strokeWidth={2}
|
|
25
|
+
>
|
|
26
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
27
|
+
</svg>
|
|
28
|
+
</button>
|
|
29
|
+
<AnimatePresence>
|
|
30
|
+
{open && (
|
|
31
|
+
<motion.div
|
|
32
|
+
initial={{ height: 0, opacity: 0 }}
|
|
33
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
34
|
+
exit={{ height: 0, opacity: 0 }}
|
|
35
|
+
transition={{ duration: 0.2 }}
|
|
36
|
+
className="overflow-hidden"
|
|
37
|
+
>
|
|
38
|
+
<p className="pb-5 text-sm text-text-secondary leading-relaxed">
|
|
39
|
+
{answer}
|
|
40
|
+
</p>
|
|
41
|
+
</motion.div>
|
|
42
|
+
)}
|
|
43
|
+
</AnimatePresence>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function FAQ() {
|
|
49
|
+
const faq = config.site.faq as { question: string; answer: string }[];
|
|
50
|
+
if (!faq || faq.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<section className="py-24 px-6">
|
|
54
|
+
<div className="max-w-2xl mx-auto">
|
|
55
|
+
<motion.div
|
|
56
|
+
className="text-center mb-12"
|
|
57
|
+
initial={{ opacity: 0, y: 20 }}
|
|
58
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
59
|
+
viewport={{ once: true }}
|
|
60
|
+
transition={{ duration: 0.5 }}
|
|
61
|
+
>
|
|
62
|
+
<h2 className="text-3xl md:text-4xl font-bold tracking-tight">
|
|
63
|
+
Frequently asked questions
|
|
64
|
+
</h2>
|
|
65
|
+
</motion.div>
|
|
66
|
+
|
|
67
|
+
<motion.div
|
|
68
|
+
className="border-t border-border"
|
|
69
|
+
initial={{ opacity: 0, y: 20 }}
|
|
70
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
71
|
+
viewport={{ once: true }}
|
|
72
|
+
transition={{ duration: 0.5, delay: 0.1 }}
|
|
73
|
+
>
|
|
74
|
+
{faq.map((item) => (
|
|
75
|
+
<FAQItem key={item.question} question={item.question} answer={item.answer} />
|
|
76
|
+
))}
|
|
77
|
+
</motion.div>
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
);
|
|
81
|
+
}
|