looda3131 commited on
Commit
a62eedc
·
1 Parent(s): 253f92e

Initial prototype

Browse files
.idx/icon.png ADDED
.modified ADDED
File without changes
docs/blueprint.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # **App Name**: ProtoChat
2
+
3
+ ## Core Features:
4
+
5
+ - Real-time Chat Interface: Provide a clean and intuitive interface for users to engage in real-time conversations.
6
+ - AI-Powered Message Generation: Utilize generative AI to create message suggestions, offering users creative prompts. The LLM will act as a tool to suggest appropriate phrases for the current message thread.
7
+ - Markdown Support: Enable users to format their messages using Markdown for richer text styling.
8
+ - Theming Options: Allow users to customize the appearance of the chat interface with light and dark themes.
9
+ - Context Aware Suggestions: Use the past chat messages as context to produce more precise AI suggestions.
10
+
11
+ ## Style Guidelines:
12
+
13
+ - Primary color: HSL 215, 70%, 50% (Hex: #3385FF) to evoke trust and intelligence.
14
+ - Background color: HSL 215, 10%, 95% (Hex: #F2F4FF) for a clean and light aesthetic.
15
+ - Accent color: HSL 185, 60%, 40% (Hex: #33A6A6) for interactive elements and highlights.
16
+ - Body and headline font: 'Inter' sans-serif for a clean and modern user experience.
17
+ - Use simple, outline-style icons to maintain a minimalist design.
18
+ - Emphasize a clean and straightforward layout to focus on conversation flow.
19
+ - Implement subtle animations for loading indicators and user interactions to provide a polished feel.
src/ai/dev.ts CHANGED
@@ -1 +1,5 @@
1
- // Flows will be imported for their side effects in this file.
 
 
 
 
 
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ import '@/ai/flows/ai-context-aware-suggestions.ts';
5
+ import '@/ai/flows/ai-suggest-message.ts';
src/ai/flows/ai-context-aware-suggestions.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+ /**
3
+ * @fileOverview AI-powered, context-aware message suggestions.
4
+ *
5
+ * - `getContextAwareSuggestions` - Generates message suggestions based on the conversation history.
6
+ * - `ContextAwareSuggestionsInput` - The input type for the `getContextAwareSuggestions` function.
7
+ * - `ContextAwareSuggestionsOutput` - The return type for the `getContextAwareSuggestions` function.
8
+ */
9
+
10
+ import {ai} from '@/ai/genkit';
11
+ import {z} from 'genkit';
12
+
13
+ const ContextAwareSuggestionsInputSchema = z.object({
14
+ chatHistory: z
15
+ .array(z.string())
16
+ .describe('The history of messages in the chat.'),
17
+ currentMessage: z.string().describe('The current message being composed.'),
18
+ });
19
+
20
+ export type ContextAwareSuggestionsInput = z.infer<
21
+ typeof ContextAwareSuggestionsInputSchema
22
+ >;
23
+
24
+ const ContextAwareSuggestionsOutputSchema = z.object({
25
+ suggestions: z
26
+ .array(z.string())
27
+ .describe('A list of suggested messages based on the context.'),
28
+ });
29
+
30
+ export type ContextAwareSuggestionsOutput = z.infer<
31
+ typeof ContextAwareSuggestionsOutputSchema
32
+ >;
33
+
34
+ export async function getContextAwareSuggestions(
35
+ input: ContextAwareSuggestionsInput
36
+ ): Promise<ContextAwareSuggestionsOutput> {
37
+ return contextAwareSuggestionsFlow(input);
38
+ }
39
+
40
+ const prompt = ai.definePrompt({
41
+ name: 'contextAwareSuggestionsPrompt',
42
+ input: {schema: ContextAwareSuggestionsInputSchema},
43
+ output: {schema: ContextAwareSuggestionsOutputSchema},
44
+ prompt: `You are an AI assistant that provides helpful message suggestions in a chat application. Consider the chat history to produce contextually relevant suggestions.
45
+
46
+ Chat History:
47
+ {{#each chatHistory}}
48
+ - {{{this}}}
49
+ {{/each}}
50
+
51
+ Current Message: {{{currentMessage}}}
52
+
53
+ Suggestions:`,
54
+ });
55
+
56
+ const contextAwareSuggestionsFlow = ai.defineFlow(
57
+ {
58
+ name: 'contextAwareSuggestionsFlow',
59
+ inputSchema: ContextAwareSuggestionsInputSchema,
60
+ outputSchema: ContextAwareSuggestionsOutputSchema,
61
+ },
62
+ async input => {
63
+ const {output} = await prompt(input);
64
+ return output!;
65
+ }
66
+ );
src/ai/flows/ai-suggest-message.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ /**
4
+ * @fileOverview An AI agent to suggest relevant phrases based on the current chat thread.
5
+ *
6
+ * - suggestMessage - A function that suggests messages based on the chat thread.
7
+ * - SuggestMessageInput - The input type for the suggestMessage function.
8
+ * - SuggestMessageOutput - The return type for the suggestMessage function.
9
+ */
10
+
11
+ import {ai} from '@/ai/genkit';
12
+ import {z} from 'genkit';
13
+
14
+ const SuggestMessageInputSchema = z.object({
15
+ chatThread: z.string().describe('The current chat thread.'),
16
+ });
17
+ export type SuggestMessageInput = z.infer<typeof SuggestMessageInputSchema>;
18
+
19
+ const SuggestMessageOutputSchema = z.object({
20
+ suggestions: z.array(z.string()).describe('An array of suggested phrases.'),
21
+ });
22
+ export type SuggestMessageOutput = z.infer<typeof SuggestMessageOutputSchema>;
23
+
24
+ export async function suggestMessage(input: SuggestMessageInput): Promise<SuggestMessageOutput> {
25
+ return suggestMessageFlow(input);
26
+ }
27
+
28
+ const prompt = ai.definePrompt({
29
+ name: 'suggestMessagePrompt',
30
+ input: {schema: SuggestMessageInputSchema},
31
+ output: {schema: SuggestMessageOutputSchema},
32
+ prompt: `You are a helpful AI assistant that suggests relevant phrases based on the current chat thread.
33
+
34
+ Here is the current chat thread:
35
+ {{chatThread}}
36
+
37
+ Suggest 3 short phrases that the user might want to say next to keep the conversation flowing.
38
+ Return the suggestions as a JSON array of strings.
39
+ `,
40
+ });
41
+
42
+ const suggestMessageFlow = ai.defineFlow(
43
+ {
44
+ name: 'suggestMessageFlow',
45
+ inputSchema: SuggestMessageInputSchema,
46
+ outputSchema: SuggestMessageOutputSchema,
47
+ },
48
+ async input => {
49
+ const {output} = await prompt(input);
50
+ return output!;
51
+ }
52
+ );
src/app/globals.css CHANGED
@@ -2,31 +2,27 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
- body {
6
- font-family: Arial, Helvetica, sans-serif;
7
- }
8
-
9
  @layer base {
10
  :root {
11
- --background: 0 0% 100%;
12
- --foreground: 0 0% 3.9%;
13
  --card: 0 0% 100%;
14
- --card-foreground: 0 0% 3.9%;
15
  --popover: 0 0% 100%;
16
- --popover-foreground: 0 0% 3.9%;
17
- --primary: 0 0% 9%;
18
- --primary-foreground: 0 0% 98%;
19
- --secondary: 0 0% 96.1%;
20
- --secondary-foreground: 0 0% 9%;
21
- --muted: 0 0% 96.1%;
22
- --muted-foreground: 0 0% 45.1%;
23
- --accent: 0 0% 96.1%;
24
- --accent-foreground: 0 0% 9%;
25
  --destructive: 0 84.2% 60.2%;
26
  --destructive-foreground: 0 0% 98%;
27
- --border: 0 0% 89.8%;
28
- --input: 0 0% 89.8%;
29
- --ring: 0 0% 3.9%;
30
  --chart-1: 12 76% 61%;
31
  --chart-2: 173 58% 39%;
32
  --chart-3: 197 37% 24%;
@@ -43,25 +39,25 @@ body {
43
  --sidebar-ring: 217.2 91.2% 59.8%;
44
  }
45
  .dark {
46
- --background: 0 0% 3.9%;
47
- --foreground: 0 0% 98%;
48
- --card: 0 0% 3.9%;
49
- --card-foreground: 0 0% 98%;
50
- --popover: 0 0% 3.9%;
51
- --popover-foreground: 0 0% 98%;
52
- --primary: 0 0% 98%;
53
- --primary-foreground: 0 0% 9%;
54
- --secondary: 0 0% 14.9%;
55
- --secondary-foreground: 0 0% 98%;
56
- --muted: 0 0% 14.9%;
57
- --muted-foreground: 0 0% 63.9%;
58
- --accent: 0 0% 14.9%;
59
- --accent-foreground: 0 0% 98%;
60
  --destructive: 0 62.8% 30.6%;
61
  --destructive-foreground: 0 0% 98%;
62
- --border: 0 0% 14.9%;
63
- --input: 0 0% 14.9%;
64
- --ring: 0 0% 83.1%;
65
  --chart-1: 220 70% 50%;
66
  --chart-2: 160 60% 45%;
67
  --chart-3: 30 80% 55%;
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
 
 
 
 
5
  @layer base {
6
  :root {
7
+ --background: 215 10% 95%;
8
+ --foreground: 222.2 84% 4.9%;
9
  --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
  --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 215 70% 50%;
14
+ --primary-foreground: 0 0% 100%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 185 60% 40%;
20
+ --accent-foreground: 0 0% 100%;
21
  --destructive: 0 84.2% 60.2%;
22
  --destructive-foreground: 0 0% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 215 70% 50%;
26
  --chart-1: 12 76% 61%;
27
  --chart-2: 173 58% 39%;
28
  --chart-3: 197 37% 24%;
 
39
  --sidebar-ring: 217.2 91.2% 59.8%;
40
  }
41
  .dark {
42
+ --background: 222.2 84% 4.9%;
43
+ --foreground: 210 40% 98%;
44
+ --card: 222.2 84% 4.9%;
45
+ --card-foreground: 210 40% 98%;
46
+ --popover: 222.2 84% 4.9%;
47
+ --popover-foreground: 210 40% 98%;
48
+ --primary: 215 70% 50%;
49
+ --primary-foreground: 0 0% 100%;
50
+ --secondary: 217.2 32.6% 17.5%;
51
+ --secondary-foreground: 210 40% 98%;
52
+ --muted: 217.2 32.6% 17.5%;
53
+ --muted-foreground: 215 20.2% 65.1%;
54
+ --accent: 185 60% 40%;
55
+ --accent-foreground: 0 0% 100%;
56
  --destructive: 0 62.8% 30.6%;
57
  --destructive-foreground: 0 0% 98%;
58
+ --border: 217.2 32.6% 17.5%;
59
+ --input: 217.2 32.6% 17.5%;
60
+ --ring: 215 70% 50%;
61
  --chart-1: 220 70% 50%;
62
  --chart-2: 160 60% 45%;
63
  --chart-3: 30 80% 55%;
src/app/layout.tsx CHANGED
@@ -1,9 +1,11 @@
1
- import type {Metadata} from 'next';
 
 
2
  import './globals.css';
3
 
4
  export const metadata: Metadata = {
5
- title: 'Firebase Studio App',
6
- description: 'Generated by Firebase Studio',
7
  };
8
 
9
  export default function RootLayout({
@@ -12,13 +14,21 @@ export default function RootLayout({
12
  children: React.ReactNode;
13
  }>) {
14
  return (
15
- <html lang="en">
16
  <head>
17
  <link rel="preconnect" href="https://fonts.googleapis.com" />
18
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
19
- <link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet" />
 
 
 
20
  </head>
21
- <body className="font-body antialiased">{children}</body>
 
 
 
 
 
22
  </html>
23
  );
24
  }
 
1
+ import type { Metadata } from 'next';
2
+ import { ThemeProvider } from '@/components/theme-provider';
3
+ import { Toaster } from '@/components/ui/toaster';
4
  import './globals.css';
5
 
6
  export const metadata: Metadata = {
7
+ title: 'ProtoChat',
8
+ description: 'An AI-powered chat application',
9
  };
10
 
11
  export default function RootLayout({
 
14
  children: React.ReactNode;
15
  }>) {
16
  return (
17
+ <html lang="en" suppressHydrationWarning>
18
  <head>
19
  <link rel="preconnect" href="https://fonts.googleapis.com" />
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
21
+ <link
22
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
23
+ rel="stylesheet"
24
+ />
25
  </head>
26
+ <body className="font-body antialiased">
27
+ <ThemeProvider>
28
+ {children}
29
+ <Toaster />
30
+ </ThemeProvider>
31
+ </body>
32
  </html>
33
  );
34
  }
src/app/page.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  export default function Home() {
2
- return <></>;
3
  }
 
1
+ import { Chat } from '@/components/chat/chat';
2
+
3
  export default function Home() {
4
+ return <Chat />;
5
  }
src/components/chat/chat.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { Message } from '@/lib/types';
5
+ import { MessageList } from './message-list';
6
+ import { MessageForm } from './message-form';
7
+ import { ThemeToggle } from '@/components/theme-toggle';
8
+ import { MessageSquare } from 'lucide-react';
9
+ import { useToast } from '@/hooks/use-toast';
10
+
11
+ export function Chat() {
12
+ const [messages, setMessages] = useState<Message[]>([
13
+ { id: '1', role: 'assistant', content: "Hello! I'm Proto, your AI assistant. How can I help you today?" }
14
+ ]);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const { toast } = useToast();
17
+
18
+ const handleNewMessage = async (message: string) => {
19
+ const userMessage: Message = { id: crypto.randomUUID(), role: 'user', content: message };
20
+ const newMessages = [...messages, userMessage];
21
+ setMessages(newMessages);
22
+ setIsLoading(true);
23
+
24
+ try {
25
+ // Simulate an AI response.
26
+ await new Promise(resolve => setTimeout(resolve, 1500));
27
+
28
+ const assistantMessage: Message = {
29
+ id: crypto.randomUUID(),
30
+ role: 'assistant',
31
+ content: `This is a simulated response to your message: "${message}". I support basic **Markdown** formatting, like *italics* and \`inline code\`.`,
32
+ };
33
+ setMessages((prev) => [...prev, assistantMessage]);
34
+ } catch (error) {
35
+ console.error('Error getting AI response:', error);
36
+ toast({
37
+ title: 'Error',
38
+ description: 'Failed to get AI response. Please try again.',
39
+ variant: 'destructive',
40
+ });
41
+ // Optionally remove the user's message if the AI fails
42
+ setMessages(messages);
43
+ } finally {
44
+ setIsLoading(false);
45
+ }
46
+ };
47
+
48
+ return (
49
+ <div className="flex h-screen w-full flex-col bg-background">
50
+ <header className="flex h-16 shrink-0 items-center justify-between border-b px-4 md:px-6">
51
+ <div className="flex items-center gap-3">
52
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary">
53
+ <MessageSquare className="h-5 w-5" />
54
+ </div>
55
+ <h1 className="text-xl font-semibold tracking-tight">ProtoChat</h1>
56
+ </div>
57
+ <ThemeToggle />
58
+ </header>
59
+ <div className="flex-1 overflow-hidden">
60
+ <MessageList messages={messages} isLoading={isLoading} />
61
+ </div>
62
+ <div className="border-t bg-card p-4 md:p-6">
63
+ <MessageForm onNewMessage={handleNewMessage} isLoading={isLoading} messages={messages} />
64
+ </div>
65
+ </div>
66
+ );
67
+ }
src/components/chat/message-form.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useRef, type FormEvent } from 'react';
4
+ import { Textarea } from '@/components/ui/textarea';
5
+ import { Button } from '@/components/ui/button';
6
+ import { ArrowUp, Loader2 } from 'lucide-react';
7
+ import { Suggestions } from './suggestions';
8
+ import type { Message } from '@/lib/types';
9
+
10
+ interface MessageFormProps {
11
+ onNewMessage: (message: string) => void;
12
+ isLoading: boolean;
13
+ messages: Message[];
14
+ }
15
+
16
+ export function MessageForm({ onNewMessage, isLoading, messages }: MessageFormProps) {
17
+ const [input, setInput] = useState('');
18
+ const inputRef = useRef<HTMLTextAreaElement>(null);
19
+
20
+ const handleSubmit = (e: FormEvent) => {
21
+ e.preventDefault();
22
+ if (input.trim()) {
23
+ onNewMessage(input.trim());
24
+ setInput('');
25
+ inputRef.current?.focus();
26
+ }
27
+ };
28
+
29
+ const handleSuggestionClick = (suggestion: string) => {
30
+ onNewMessage(suggestion);
31
+ setInput('');
32
+ }
33
+
34
+ return (
35
+ <div className="mx-auto w-full max-w-3xl space-y-4">
36
+ <Suggestions onSuggestionClick={handleSuggestionClick} messages={messages} isLoading={isLoading} />
37
+ <form onSubmit={handleSubmit} className="relative">
38
+ <Textarea
39
+ ref={inputRef}
40
+ value={input}
41
+ onChange={(e) => setInput(e.target.value)}
42
+ onKeyDown={(e) => {
43
+ if (e.key === 'Enter' && !e.shiftKey) {
44
+ e.preventDefault();
45
+ handleSubmit(e);
46
+ }
47
+ }}
48
+ placeholder="Send a message..."
49
+ className="min-h-[48px] resize-none pr-16"
50
+ disabled={isLoading}
51
+ autoFocus
52
+ />
53
+ <Button
54
+ type="submit"
55
+ size="icon"
56
+ className="absolute bottom-3 right-3 h-8 w-8"
57
+ disabled={isLoading || !input.trim()}
58
+ aria-label="Send message"
59
+ >
60
+ {isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-4 w-4" />}
61
+ </Button>
62
+ </form>
63
+ </div>
64
+ );
65
+ }
src/components/chat/message-list.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import type { Message as MessageType } from '@/lib/types';
4
+ import { Message } from './message';
5
+ import { Skeleton } from '@/components/ui/skeleton';
6
+ import { useEffect, useRef } from 'react';
7
+ import { Avatar, AvatarFallback } from '../ui/avatar';
8
+
9
+ interface MessageListProps {
10
+ messages: MessageType[];
11
+ isLoading: boolean;
12
+ }
13
+
14
+ export function MessageList({ messages, isLoading }: MessageListProps) {
15
+ const scrollRef = useRef<HTMLDivElement>(null);
16
+
17
+ useEffect(() => {
18
+ if (scrollRef.current) {
19
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
20
+ }
21
+ }, [messages, isLoading]);
22
+
23
+ return (
24
+ <div ref={scrollRef} className="h-full overflow-y-auto px-4 pt-6 md:px-6">
25
+ <div className="mx-auto max-w-3xl space-y-6">
26
+ {messages.map((message) => <Message key={message.id} message={message} />)}
27
+ {isLoading && (
28
+ <div className="flex items-start gap-4">
29
+ <Avatar className="h-8 w-8 border">
30
+ <AvatarFallback className="bg-primary/10 text-primary">AI</AvatarFallback>
31
+ </Avatar>
32
+ <div className="grid gap-2 pt-2">
33
+ <Skeleton className="h-4 w-48" />
34
+ <Skeleton className="h-4 w-32" />
35
+ </div>
36
+ </div>
37
+ )}
38
+ </div>
39
+ </div>
40
+ );
41
+ }
src/components/chat/message.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from '@/lib/utils';
2
+ import type { Message as MessageType } from '@/lib/types';
3
+ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
4
+ import { Markdown } from '@/components/markdown';
5
+ import { User, Bot } from 'lucide-react';
6
+
7
+ interface MessageProps {
8
+ message: MessageType;
9
+ }
10
+
11
+ export function Message({ message }: MessageProps) {
12
+ const isUser = message.role === 'user';
13
+
14
+ return (
15
+ <div className={cn('flex items-start gap-4', isUser && 'justify-end')}>
16
+ {!isUser && (
17
+ <Avatar className="h-8 w-8 border">
18
+ <AvatarFallback className="bg-primary/10 text-primary">
19
+ <Bot className="h-5 w-5" />
20
+ </AvatarFallback>
21
+ </Avatar>
22
+ )}
23
+ <div
24
+ className={cn(
25
+ 'max-w-[75%] rounded-2xl p-3 px-4',
26
+ isUser
27
+ ? 'rounded-br-none bg-primary text-primary-foreground'
28
+ : 'rounded-bl-none bg-muted'
29
+ )}
30
+ >
31
+ <Markdown content={message.content} />
32
+ </div>
33
+ {isUser && (
34
+ <Avatar className="h-8 w-8 border">
35
+ <AvatarFallback className="bg-secondary">
36
+ <User className="h-5 w-5" />
37
+ </AvatarFallback>
38
+ </Avatar>
39
+ )}
40
+ </div>
41
+ );
42
+ }
src/components/chat/suggestions.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { suggestMessage } from '@/ai/flows/ai-suggest-message';
6
+ import { Skeleton } from '@/components/ui/skeleton';
7
+ import { Sparkles } from 'lucide-react';
8
+ import type { Message } from '@/lib/types';
9
+ import { useToast } from '@/hooks/use-toast';
10
+
11
+ interface SuggestionsProps {
12
+ onSuggestionClick: (suggestion: string) => void;
13
+ messages: Message[];
14
+ isLoading: boolean;
15
+ }
16
+
17
+ export function Suggestions({ onSuggestionClick, messages, isLoading }: SuggestionsProps) {
18
+ const [suggestions, setSuggestions] = useState<string[]>([]);
19
+ const [loading, setLoading] = useState(false);
20
+ const { toast } = useToast();
21
+
22
+ useEffect(() => {
23
+ // A simple way to prevent fetching on every message while AI is responding
24
+ const timer = setTimeout(() => {
25
+ if (isLoading) return;
26
+
27
+ async function fetchSuggestions() {
28
+ setLoading(true);
29
+ try {
30
+ const chatThread = messages.map(m => `${m.role}: ${m.content}`).join('\n');
31
+ const response = await suggestMessage({ chatThread: chatThread || 'Start a new conversation.' });
32
+
33
+ if (response.suggestions && response.suggestions.length > 0) {
34
+ setSuggestions(response.suggestions);
35
+ } else {
36
+ setSuggestions([]);
37
+ }
38
+ } catch (error) {
39
+ console.error('Failed to fetch suggestions:', error);
40
+ // Only show toast on first load failure maybe
41
+ if (messages.length <= 1) {
42
+ toast({ title: 'Suggestion Error', description: 'Could not load AI suggestions.', variant: 'destructive' });
43
+ }
44
+ setSuggestions([]);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ }
49
+
50
+ fetchSuggestions();
51
+ }, 500); // Debounce fetching
52
+
53
+ return () => clearTimeout(timer);
54
+ }, [messages, isLoading, toast]);
55
+
56
+ if (loading) {
57
+ return (
58
+ <div className="flex h-8 animate-pulse items-center gap-2">
59
+ <Sparkles className="h-5 w-5 text-muted-foreground" />
60
+ <Skeleton className="h-8 w-32 rounded-full" />
61
+ <Skeleton className="h-8 w-40 rounded-full" />
62
+ <Skeleton className="h-8 w-28 rounded-full" />
63
+ </div>
64
+ );
65
+ }
66
+
67
+ if (suggestions.length === 0 || isLoading) {
68
+ return <div className="h-8" />; // Maintain layout consistency
69
+ }
70
+
71
+ return (
72
+ <div className="flex h-8 items-center gap-2">
73
+ <Sparkles className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
74
+ <div className="flex gap-2 overflow-x-auto pb-2">
75
+ {suggestions.map((suggestion, index) => (
76
+ <Button
77
+ key={index}
78
+ variant="outline"
79
+ size="sm"
80
+ className="rounded-full whitespace-nowrap"
81
+ onClick={() => onSuggestionClick(suggestion)}
82
+ >
83
+ {suggestion}
84
+ </Button>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ );
89
+ }
src/components/markdown.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ export const Markdown = ({ content, className }: { content: string, className?: string }) => {
5
+ const parts = content.split(/(\*\*.*?\*\*|\*.*?\*|`.*?`|```[\s\S]*?```)/g);
6
+
7
+ return (
8
+ <div className={cn("prose dark:prose-invert", className)}>
9
+ {parts.map((part, index) => {
10
+ if (part.startsWith('**') && part.endsWith('**')) {
11
+ return <strong key={index}>{part.slice(2, -2)}</strong>;
12
+ }
13
+ if (part.startsWith('*') && part.endsWith('*')) {
14
+ return <em key={index}>{part.slice(1, -1)}</em>;
15
+ }
16
+ if (part.startsWith('```') && part.endsWith('```')) {
17
+ return (
18
+ <pre className="whitespace-pre-wrap bg-muted p-2 rounded-md my-2" key={index}>
19
+ <code>{part.slice(3, -3)}</code>
20
+ </pre>
21
+ );
22
+ }
23
+ if (part.startsWith('`') && part.endsWith('`')) {
24
+ return <code className="bg-muted text-foreground rounded px-1 py-0.5" key={index}>{part.slice(1, -1)}</code>;
25
+ }
26
+ return <span key={index}>{part}</span>;
27
+ })}
28
+ </div>
29
+ );
30
+ };
src/components/theme-provider.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
4
+
5
+ type Theme = "dark" | "light" | "system";
6
+
7
+ type ThemeProviderProps = {
8
+ children: ReactNode;
9
+ };
10
+
11
+ type ThemeProviderState = {
12
+ theme: Theme;
13
+ setTheme: (theme: Theme) => void;
14
+ };
15
+
16
+ const initialState: ThemeProviderState = {
17
+ theme: "system",
18
+ setTheme: () => null,
19
+ };
20
+
21
+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
22
+
23
+ export function ThemeProvider({
24
+ children,
25
+ ...props
26
+ }: ThemeProviderProps) {
27
+ const [theme, setTheme] = useState<Theme>("system");
28
+
29
+ useEffect(() => {
30
+ const storedTheme = localStorage.getItem("protochat-theme") as Theme | null;
31
+ if (storedTheme) {
32
+ setTheme(storedTheme);
33
+ }
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ const root = window.document.documentElement;
38
+
39
+ root.classList.remove("light", "dark");
40
+
41
+ let currentTheme = theme;
42
+ if (currentTheme === "system") {
43
+ currentTheme = window.matchMedia("(prefers-color-scheme: dark)")
44
+ .matches
45
+ ? "dark"
46
+ : "light";
47
+ }
48
+
49
+ root.classList.add(currentTheme);
50
+ }, [theme]);
51
+
52
+ const value = {
53
+ theme,
54
+ setTheme: (newTheme: Theme) => {
55
+ localStorage.setItem("protochat-theme", newTheme);
56
+ setTheme(newTheme);
57
+ },
58
+ };
59
+
60
+ return (
61
+ <ThemeProviderContext.Provider {...props} value={value}>
62
+ {children}
63
+ </ThemeProviderContext.Provider>
64
+ );
65
+ }
66
+
67
+ export const useTheme = () => {
68
+ const context = useContext(ThemeProviderContext);
69
+
70
+ if (context === undefined)
71
+ throw new Error("useTheme must be used within a ThemeProvider");
72
+
73
+ return context;
74
+ };
src/components/theme-toggle.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Moon, Sun } from "lucide-react"
5
+
6
+ import { useTheme } from "@/components/theme-provider"
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu"
14
+
15
+ export function ThemeToggle() {
16
+ const { setTheme } = useTheme()
17
+
18
+ return (
19
+ <DropdownMenu>
20
+ <DropdownMenuTrigger asChild>
21
+ <Button variant="ghost" size="icon">
22
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
23
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
24
+ <span className="sr-only">Toggle theme</span>
25
+ </Button>
26
+ </DropdownMenuTrigger>
27
+ <DropdownMenuContent align="end">
28
+ <DropdownMenuItem onClick={() => setTheme("light")}>
29
+ Light
30
+ </DropdownMenuItem>
31
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
32
+ Dark
33
+ </DropdownMenuItem>
34
+ <DropdownMenuItem onClick={() => setTheme("system")}>
35
+ System
36
+ </DropdownMenuItem>
37
+ </DropdownMenuContent>
38
+ </DropdownMenu>
39
+ )
40
+ }
src/lib/types.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface Message {
2
+ id: string;
3
+ role: 'user' | 'assistant';
4
+ content: string;
5
+ }