Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions intelligence/ts/examples/nextjs-web-chats/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import { chatWithHistory } from '@/lib/chat';
import { chatWithHistory, chatWithMessages } from '@/lib/chat';
import type { Message } from '@flwr/flwr';

export async function POST(req: Request) {
try {
const { question } = await req.json();
const { question, messages } = (await req.json()) as {
question?: string;
messages?: Message[];
};

if (Array.isArray(messages) && messages.length > 0) {
const responseMessage = await chatWithMessages(messages);
return new Response(
JSON.stringify({ message: responseMessage.content, role: responseMessage.role }),
{ status: 200 }
);
}

if (!question?.trim()) {
return new Response(JSON.stringify({ error: 'Missing question.' }), { status: 400 });
}

const message = await chatWithHistory(question);
return new Response(JSON.stringify({ message }), { status: 200 });
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,75 @@
'use client';

import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { FlowerIntelligence, ChatResponseResult, ChatOptions, Message } from '@flwr/flwr';

const fi: FlowerIntelligence = FlowerIntelligence.instance;
type ChatMessageRole = 'system' | 'user' | 'assistant';

const history: Message[] = [
{ role: 'system', content: 'You are a friendly assistant that loves using emojis.' },
];
interface ChatMessage {
role: ChatMessageRole;
content: string;
}

interface ChatEntry {
role: 'user' | 'bot';
content: string;
}

export default function ClientSideChatPage() {
// Initialize local state using the current global history (excluding the system message)
const [chatLog, setChatLog] = useState<ChatEntry[]>(
history
.filter((msg) => msg.role !== 'system')
.map((msg) => ({
role: msg.role as 'user' | 'bot',
content: msg.content,
}))
);
const [messages, setMessages] = useState<ChatMessage[]>([
{ role: 'system', content: 'You are a friendly assistant that loves using emojis.' },
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);

const chatLog = useMemo<ChatEntry[]>(
() =>
messages
.filter((msg) => msg.role !== 'system')
.map((msg) => ({
role: msg.role === 'user' ? 'user' : 'bot',
content: msg.content,
})),
[messages]
);

const sendQuestion = async () => {
if (!input.trim()) return;
const trimmed = input.trim();
if (!trimmed) return;
setLoading(true);
setChatLog((prev) => [...prev, { role: 'user', content: input }]);
setInput('');

const nextMessages: ChatMessage[] = [...messages, { role: 'user', content: trimmed }];
setMessages(nextMessages);

// Append user's question to the global history.
history.push({ role: 'user', content: input });
try {
// Call the FlowerIntelligence client directly with updated history.
const response: ChatResponseResult = await fi.chat(input, {
messages: history,
} as ChatOptions);
if (response.ok) {
history.push(response.message);
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: nextMessages }),
});
const data = (await res.json()) as {
message?: string;
role?: ChatMessageRole;
error?: string;
};
if (res.ok && data.message) {
const message = data.message;
setMessages((prev) => [...prev, { role: data.role ?? 'assistant', content: message }]);
} else {
history.push({ role: 'bot', content: 'Failed to get a valid response.' });
setMessages((prev) => [
...prev,
{ role: 'assistant', content: data.error || 'Failed to get a valid response.' },
]);
}
} catch {
setChatLog([
...history
.filter((msg) => msg.role !== 'system')
.map((msg) => ({
role: msg.role as 'user' | 'bot',
content: msg.content,
})),
]);
} finally {
// Update local chat log to reflect the global history (filtering out the system message).
setChatLog([
...history
.filter((msg) => msg.role !== 'system')
.map((msg) => ({
role: msg.role as 'user' | 'bot',
content: msg.content,
})),
setMessages((prev) => [
...prev,
{ role: 'assistant', content: 'Network error, please try again.' },
]);
setInput('');
setLoading(false);
}
setLoading(false);
};

const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
20 changes: 20 additions & 0 deletions intelligence/ts/examples/nextjs-web-chats/lib/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ export const history: Message[] = [
{ role: 'system', content: 'You are a friendly assistant that loves using emojis.' },
];

export async function chatWithMessages(messages: Message[]): Promise<Message> {
try {
const response: ChatResponseResult = await fi.chat({
messages,
});
if (!response || (response.ok && !response.message)) {
throw new Error('Invalid response structure from the chat service.');
}
if (!response.ok) {
console.error(response);
throw new Error('Failed to get a valid response.');
}

return response.message;
} catch (error) {
console.error('Error in chatWithMessages:', error);
throw new Error('Failed to get a valid response from the chat service.');
}
}

export async function chatWithHistory(question: string): Promise<string> {
try {
history.push({ role: 'user', content: question });
Expand Down
5 changes: 4 additions & 1 deletion intelligence/ts/examples/nextjs-web-chats/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
/* config options here */
turbopack: {
root: process.cwd(),
},
serverExternalPackages: ['@flwr/flwr', '@huggingface/transformers', 'onnxruntime-node'],
};

export default nextConfig;
3 changes: 2 additions & 1 deletion intelligence/ts/examples/nextjs-web-chats/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dependencies": {
"@flwr/flwr": "^0.2.5",
"@tailwindcss/typography": "^0.5.19",
"next": "15.4.11",
"next": "16.1.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
Expand All @@ -23,6 +23,7 @@
"@types/node": "^22.19.7",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"baseline-browser-mapping": "^2.9.19",
"eslint": "^9.39.2",
"eslint-config-next": "15.2.4",
"tailwindcss": "^4.1.18",
Expand Down
10 changes: 8 additions & 2 deletions intelligence/ts/examples/nextjs-web-chats/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -22,6 +22,12 @@
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
Loading