create-supyagent-app 0.1.36 → 0.1.38

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.
@@ -6,10 +6,36 @@ import { ChatMessage } from "./chat-message";
6
6
  import { ChatInput } from "./chat-input";
7
7
  import { ChatSidebar } from "./chat-sidebar";
8
8
  import { useRef, useEffect, useMemo, useState, useCallback } from "react";
9
- import { ArrowDown } from "lucide-react";
9
+ import { ArrowDown, Mail, MessageSquare, Github, ExternalLink } from "lucide-react";
10
+ import { ContextIndicator } from "@supyagent/sdk/react";
10
11
  import { Header } from "./header";
11
12
  import Image from "next/image";
12
13
 
14
+ interface Integration {
15
+ provider: string;
16
+ status: string;
17
+ }
18
+
19
+ interface MeData {
20
+ integrations: Integration[];
21
+ dashboardUrl: string;
22
+ }
23
+
24
+ const PROVIDER_ICONS: Record<string, React.ReactNode> = {
25
+ google: <Mail className="h-4 w-4" />,
26
+ slack: <MessageSquare className="h-4 w-4" />,
27
+ github: <Github className="h-4 w-4" />,
28
+ discord: <MessageSquare className="h-4 w-4" />,
29
+ microsoft: <Mail className="h-4 w-4" />,
30
+ notion: <ExternalLink className="h-4 w-4" />,
31
+ };
32
+
33
+ const POPULAR_INTEGRATIONS = [
34
+ { provider: "google", label: "Google", description: "Gmail, Calendar, Drive" },
35
+ { provider: "slack", label: "Slack", description: "Messages & channels" },
36
+ { provider: "github", label: "GitHub", description: "Repos & issues" },
37
+ ];
38
+
13
39
  interface ChatProps {
14
40
  chatId: string;
15
41
  initialMessages: UIMessage[];
@@ -25,7 +51,7 @@ export function Chat({ chatId, initialMessages }: ChatProps) {
25
51
  [chatId]
26
52
  );
27
53
 
28
- const { messages, sendMessage, status, stop, addToolApprovalResponse } = useChat({
54
+ const { messages, sendMessage, status, stop, addToolApprovalResponse, setMessages } = useChat({
29
55
  id: chatId,
30
56
  transport,
31
57
  messages: initialMessages,
@@ -33,10 +59,41 @@ export function Chat({ chatId, initialMessages }: ChatProps) {
33
59
  });
34
60
 
35
61
  const isLoading = status === "submitted" || status === "streaming";
62
+ const [isCompacting, setIsCompacting] = useState(false);
63
+
64
+ const handleCompact = useCallback(async () => {
65
+ if (isCompacting || isLoading) return;
66
+ setIsCompacting(true);
67
+ try {
68
+ const res = await fetch("/api/chat/compact", {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({ chatId }),
72
+ });
73
+ if (res.ok) {
74
+ const { messages: compactedMessages } = await res.json();
75
+ setMessages(compactedMessages);
76
+ }
77
+ } catch {
78
+ // Compact failed silently
79
+ } finally {
80
+ setIsCompacting(false);
81
+ }
82
+ }, [chatId, isCompacting, isLoading, setMessages]);
36
83
 
37
84
  const scrollRef = useRef<HTMLDivElement>(null);
38
85
  const bottomRef = useRef<HTMLDivElement>(null);
39
86
  const [isAtBottom, setIsAtBottom] = useState(true);
87
+ const [meData, setMeData] = useState<MeData | null>(null);
88
+
89
+ useEffect(() => {
90
+ fetch("/api/me")
91
+ .then((res) => (res.ok ? res.json() : null))
92
+ .then((data) => {
93
+ if (data) setMeData(data);
94
+ })
95
+ .catch(() => {});
96
+ }, []);
40
97
 
41
98
  const scrollToBottom = useCallback(() => {
42
99
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -68,6 +125,9 @@ export function Chat({ chatId, initialMessages }: ChatProps) {
68
125
  "What's on my calendar today?",
69
126
  ];
70
127
 
128
+ const hasIntegrations = meData && meData.integrations.length > 0;
129
+ const dashboardUrl = meData?.dashboardUrl || "https://app.supyagent.com";
130
+
71
131
  return (
72
132
  <div className="flex h-screen flex-col">
73
133
  <Header />
@@ -82,29 +142,106 @@ export function Chat({ chatId, initialMessages }: ChatProps) {
82
142
  <div className="mx-auto max-w-3xl space-y-6">
83
143
  {messages.length === 0 && (
84
144
  <div className="flex h-full min-h-[60vh] items-center justify-center">
85
- <div className="text-center max-w-md">
86
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-muted">
87
- <Image src="/logo.png" alt="Supyagent" width={24} height={24} className="opacity-70" />
145
+ {!hasIntegrations && meData !== null ? (
146
+ <div className="text-center max-w-lg">
147
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-muted">
148
+ <Image src="/logo.png" alt="Supyagent" width={24} height={24} className="opacity-70" />
149
+ </div>
150
+ <h1 className="text-xl font-semibold text-foreground">
151
+ Get started
152
+ </h1>
153
+ <p className="mt-2 text-sm text-muted-foreground">
154
+ Connect your integrations to start using your AI agent.
155
+ </p>
156
+
157
+ <div className="mt-6 space-y-2">
158
+ <div className="flex items-center gap-3 rounded-lg border border-border bg-card p-3 text-left">
159
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground">1</span>
160
+ <div className="flex-1 min-w-0">
161
+ <p className="text-sm font-medium text-foreground">Connect integrations</p>
162
+ <p className="text-xs text-muted-foreground">Link your services to give your agent access</p>
163
+ </div>
164
+ <a
165
+ href={`${dashboardUrl}/integrations`}
166
+ target="_blank"
167
+ rel="noopener noreferrer"
168
+ className="shrink-0 rounded-md border border-border px-3 py-1 text-xs font-medium text-foreground hover:bg-muted transition-colors"
169
+ >
170
+ Open dashboard
171
+ </a>
172
+ </div>
173
+ <div className="flex items-center gap-3 rounded-lg border border-border bg-card p-3 text-left">
174
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground">2</span>
175
+ <div className="flex-1 min-w-0">
176
+ <p className="text-sm font-medium text-foreground">Try a command</p>
177
+ <p className="text-xs text-muted-foreground">Type <kbd className="rounded bg-muted px-1 py-0.5 text-[10px] font-mono">/</kbd> to see quick actions</p>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <div className="mt-6">
183
+ <p className="mb-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">Popular integrations</p>
184
+ <div className="flex justify-center gap-3">
185
+ {POPULAR_INTEGRATIONS.map((integration) => (
186
+ <a
187
+ key={integration.provider}
188
+ href={`${dashboardUrl}/integrations`}
189
+ target="_blank"
190
+ rel="noopener noreferrer"
191
+ className="flex flex-col items-center gap-1.5 rounded-lg border border-border bg-card p-3 hover:bg-muted transition-colors w-28"
192
+ >
193
+ <span className="text-muted-foreground">
194
+ {PROVIDER_ICONS[integration.provider]}
195
+ </span>
196
+ <span className="text-xs font-medium text-foreground">{integration.label}</span>
197
+ <span className="text-[10px] text-muted-foreground">{integration.description}</span>
198
+ </a>
199
+ ))}
200
+ </div>
201
+ </div>
88
202
  </div>
89
- <h1 className="text-xl font-semibold text-foreground">
90
- Supyagent Chat
91
- </h1>
92
- <p className="mt-2 text-sm text-muted-foreground">
93
- Ask me anything — I can use your connected integrations.
94
- </p>
95
- <div className="mt-6 flex flex-wrap justify-center gap-2">
96
- {suggestions.map((suggestion) => (
97
- <button
98
- key={suggestion}
99
- type="button"
100
- onClick={() => sendMessage({ text: suggestion })}
101
- className="rounded-full border border-border bg-card px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
102
- >
103
- {suggestion}
104
- </button>
105
- ))}
203
+ ) : (
204
+ <div className="text-center max-w-md">
205
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-muted">
206
+ <Image src="/logo.png" alt="Supyagent" width={24} height={24} className="opacity-70" />
207
+ </div>
208
+ <h1 className="text-xl font-semibold text-foreground">
209
+ Supyagent Chat
210
+ </h1>
211
+ <p className="mt-2 text-sm text-muted-foreground">
212
+ Ask me anything — I can use your connected integrations.
213
+ </p>
214
+
215
+ {hasIntegrations && (
216
+ <div className="mt-4 flex justify-center gap-1.5">
217
+ {meData.integrations.map((integration) => (
218
+ <span
219
+ key={integration.provider}
220
+ className="flex h-7 w-7 items-center justify-center rounded-md bg-muted text-muted-foreground"
221
+ title={integration.provider}
222
+ >
223
+ {PROVIDER_ICONS[integration.provider] || (
224
+ <span className="text-[10px] font-medium uppercase">{integration.provider.slice(0, 2)}</span>
225
+ )}
226
+ </span>
227
+ ))}
228
+ </div>
229
+ )}
230
+
231
+ <div className="mt-6 flex flex-wrap justify-center gap-2">
232
+ {suggestions.map((suggestion) => (
233
+ <button
234
+ key={suggestion}
235
+ type="button"
236
+ onClick={() => sendMessage({ text: suggestion })}
237
+ className="rounded-full border border-border bg-card px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
238
+ >
239
+ {suggestion}
240
+ </button>
241
+ ))}
242
+ </div>
106
243
  </div>
107
- </div>
244
+ )}
108
245
  </div>
109
246
  )}
110
247
  {messages.map((message) => (
@@ -128,10 +265,14 @@ export function Chat({ chatId, initialMessages }: ChatProps) {
128
265
 
129
266
  <div className="border-t border-border px-4 py-4">
130
267
  <div className="mx-auto max-w-3xl">
268
+ <div className="flex items-center justify-end mb-1.5 min-h-[20px]">
269
+ <ContextIndicator messages={messages} />
270
+ </div>
131
271
  <ChatInput
132
272
  sendMessage={sendMessage}
133
273
  isLoading={isLoading}
134
274
  stop={stop}
275
+ onCompact={handleCompact}
135
276
  />
136
277
  </div>
137
278
  </div>
@@ -1,9 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import Image from "next/image";
4
+ import { Sun, Moon } from "lucide-react";
4
5
  import { UserButton } from "./user-button";
6
+ import { useTheme } from "./theme-provider";
5
7
 
6
8
  export function Header() {
9
+ const { theme, toggleTheme } = useTheme();
10
+
7
11
  return (
8
12
  <header className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-card px-4">
9
13
  <div className="flex items-center gap-2.5">
@@ -16,7 +20,17 @@ export function Header() {
16
20
  />
17
21
  <span className="text-sm font-medium text-foreground">Supyagent</span>
18
22
  </div>
19
- <UserButton />
23
+ <div className="flex items-center gap-2">
24
+ <button
25
+ type="button"
26
+ onClick={toggleTheme}
27
+ className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
28
+ title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
29
+ >
30
+ {theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
31
+ </button>
32
+ <UserButton />
33
+ </div>
20
34
  </header>
21
35
  );
22
36
  }
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState, useCallback } from "react";
4
+ import {
5
+ Mail,
6
+ Calendar,
7
+ HardDrive,
8
+ MessageSquare,
9
+ Github,
10
+ Search,
11
+ Send,
12
+ CalendarClock,
13
+ Minimize2,
14
+ } from "lucide-react";
15
+
16
+ interface SlashCommand {
17
+ name: string;
18
+ label: string;
19
+ description: string;
20
+ prompt: string;
21
+ icon: React.ReactNode;
22
+ }
23
+
24
+ const COMMANDS: SlashCommand[] = [
25
+ { name: "email", label: "/email", description: "Search my recent emails", prompt: "Search my recent emails", icon: <Mail className="h-4 w-4" /> },
26
+ { name: "calendar", label: "/calendar", description: "What's on my calendar today?", prompt: "What's on my calendar today?", icon: <Calendar className="h-4 w-4" /> },
27
+ { name: "drive", label: "/drive", description: "Find files in my Drive", prompt: "Find files in my Drive", icon: <HardDrive className="h-4 w-4" /> },
28
+ { name: "slack", label: "/slack", description: "Check my Slack messages", prompt: "Check my Slack messages", icon: <MessageSquare className="h-4 w-4" /> },
29
+ { name: "github", label: "/github", description: "Show my recent GitHub activity", prompt: "Show my recent GitHub activity", icon: <Github className="h-4 w-4" /> },
30
+ { name: "search", label: "/search", description: "Search the web for...", prompt: "Search the web for ", icon: <Search className="h-4 w-4" /> },
31
+ { name: "compose", label: "/compose", description: "Draft an email to...", prompt: "Draft an email to ", icon: <Send className="h-4 w-4" /> },
32
+ { name: "schedule", label: "/schedule", description: "Schedule a meeting for...", prompt: "Schedule a meeting for ", icon: <CalendarClock className="h-4 w-4" /> },
33
+ { name: "compact", label: "/compact", description: "Compact conversation history", prompt: "__compact__", icon: <Minimize2 className="h-4 w-4" /> },
34
+ ];
35
+
36
+ interface SlashMenuProps {
37
+ query: string;
38
+ onSelect: (command: SlashCommand) => void;
39
+ onClose: () => void;
40
+ }
41
+
42
+ export function SlashMenu({ query, onSelect, onClose }: SlashMenuProps) {
43
+ const [selectedIndex, setSelectedIndex] = useState(0);
44
+ const menuRef = useRef<HTMLDivElement>(null);
45
+ const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
46
+
47
+ const filtered = COMMANDS.filter(
48
+ (cmd) =>
49
+ cmd.name.startsWith(query.toLowerCase()) ||
50
+ cmd.description.toLowerCase().includes(query.toLowerCase())
51
+ );
52
+
53
+ // Reset selection when filtered list changes
54
+ useEffect(() => {
55
+ setSelectedIndex(0);
56
+ }, [query]);
57
+
58
+ // Scroll selected item into view
59
+ useEffect(() => {
60
+ itemRefs.current[selectedIndex]?.scrollIntoView({ block: "nearest" });
61
+ }, [selectedIndex]);
62
+
63
+ const handleKeyDown = useCallback(
64
+ (e: KeyboardEvent) => {
65
+ if (filtered.length === 0) return;
66
+
67
+ switch (e.key) {
68
+ case "ArrowDown":
69
+ e.preventDefault();
70
+ setSelectedIndex((prev) => (prev + 1) % filtered.length);
71
+ break;
72
+ case "ArrowUp":
73
+ e.preventDefault();
74
+ setSelectedIndex((prev) => (prev - 1 + filtered.length) % filtered.length);
75
+ break;
76
+ case "Enter":
77
+ e.preventDefault();
78
+ onSelect(filtered[selectedIndex]);
79
+ break;
80
+ case "Escape":
81
+ e.preventDefault();
82
+ onClose();
83
+ break;
84
+ }
85
+ },
86
+ [filtered, selectedIndex, onSelect, onClose]
87
+ );
88
+
89
+ useEffect(() => {
90
+ document.addEventListener("keydown", handleKeyDown);
91
+ return () => document.removeEventListener("keydown", handleKeyDown);
92
+ }, [handleKeyDown]);
93
+
94
+ if (filtered.length === 0) return null;
95
+
96
+ return (
97
+ <div
98
+ ref={menuRef}
99
+ className="absolute bottom-full left-0 right-0 mb-2 max-h-64 overflow-y-auto rounded-xl border border-border bg-card shadow-lg"
100
+ >
101
+ <div className="p-1">
102
+ {filtered.map((cmd, i) => (
103
+ <button
104
+ key={cmd.name}
105
+ ref={(el) => { itemRefs.current[i] = el; }}
106
+ type="button"
107
+ onClick={() => onSelect(cmd)}
108
+ onMouseEnter={() => setSelectedIndex(i)}
109
+ className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors ${
110
+ i === selectedIndex
111
+ ? "bg-muted text-foreground"
112
+ : "text-muted-foreground hover:bg-muted/50"
113
+ }`}
114
+ >
115
+ <span className="shrink-0 text-muted-foreground">{cmd.icon}</span>
116
+ <div className="flex-1 min-w-0">
117
+ <span className="text-sm font-medium">{cmd.label}</span>
118
+ <span className="ml-2 text-xs text-muted-foreground">{cmd.description}</span>
119
+ </div>
120
+ </button>
121
+ ))}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ export type { SlashCommand };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from "react";
4
+
5
+ type Theme = "light" | "dark";
6
+
7
+ interface ThemeContextValue {
8
+ theme: Theme;
9
+ toggleTheme: () => void;
10
+ }
11
+
12
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
13
+
14
+ function getStoredTheme(): Theme {
15
+ if (typeof window === "undefined") return "dark";
16
+ const stored = localStorage.getItem("theme");
17
+ return stored === "light" || stored === "dark" ? stored : "dark";
18
+ }
19
+
20
+ function applyTheme(theme: Theme) {
21
+ const root = document.documentElement;
22
+ if (theme === "dark") {
23
+ root.classList.add("dark");
24
+ } else {
25
+ root.classList.remove("dark");
26
+ }
27
+ }
28
+
29
+ export function ThemeProvider({ children }: { children: ReactNode }) {
30
+ const [theme, setTheme] = useState<Theme>("dark");
31
+
32
+ useEffect(() => {
33
+ const initial = getStoredTheme();
34
+ setTheme(initial);
35
+ applyTheme(initial);
36
+ }, []);
37
+
38
+ const toggleTheme = useCallback(() => {
39
+ setTheme((prev) => {
40
+ const next = prev === "dark" ? "light" : "dark";
41
+ localStorage.setItem("theme", next);
42
+ applyTheme(next);
43
+ return next;
44
+ });
45
+ }, []);
46
+
47
+ return (
48
+ <ThemeContext.Provider value={{ theme, toggleTheme }}>
49
+ {children}
50
+ </ThemeContext.Provider>
51
+ );
52
+ }
53
+
54
+ export function useTheme(): ThemeContextValue {
55
+ const ctx = useContext(ThemeContext);
56
+ if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
57
+ return ctx;
58
+ }