@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,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
+ }