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
11 changes: 8 additions & 3 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ io.on("connection", (socket) => {
console.log("New client connected:", socket.id);

// Receive prompt along with its unique ID from the client
socket.on("send_prompt", async ({ id, prompt }) => {
socket.on("send_prompt", async ({ id, prompt, mock }) => {
// if (mock) {
// socket.emit("ai_response", suggestionMocks);
// console.log({ action: "returned mock data", data: suggestionMocks });
// return;
// }
try {
const response = await openai.chat.completions.create({
model: "gpt-4",
Expand All @@ -60,10 +65,10 @@ io.on("connection", (socket) => {
{
role: "system",
content:
"You are overhearing a game master running a Tabletop RPG game, briefly predict and provide creative ideas for what the game master might say next. You are limited to 20 tokens",
"You are overhearing a game master running a Tabletop RPG game, briefly predict and provide creative ideas for what the game master might say next. You are limited to 30 tokens.",
},
],
max_tokens: 20,
max_tokens: 30,
temperature: 0.8,
});

Expand Down
18 changes: 7 additions & 11 deletions api/types/apiTypes.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { SendPromptData, SuggestionObj } from "suggestionTypes";

export interface ServerToClientEvents {
noArg: () => void;
basicEmit: (a: number, b: string, c: Buffer) => void;
withAck: (d: string, callback: (e: number) => void) => void;
ai_response: (data: AiResponseData) => void;
ai_response: (data: {
id: string;
response: any;
}) => void;
// ai_response: (data: SuggestionObj[]) => void;
}

export interface ClientToServerEvents {
Expand All @@ -18,13 +24,3 @@ export interface SocketData {
name: string;
age: number;
}

export interface SendPromptData {
id: string;
prompt: string;
}

export interface AiResponseData {
id: string;
response: string | null;
}
8 changes: 8 additions & 0 deletions api/types/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum SuggestionCategory {
rules = "Rules",
items = "Items",
monsters = "Monsters",
events = "Events",
scene = "Scene Description",
dialogue = "Dialogue",
}
26 changes: 26 additions & 0 deletions api/types/suggestionMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SuggestionObj } from "suggestionTypes";

import { SuggestionCategory } from "./enums";

export const suggestionMocks: SuggestionObj[] = [
{
id: "number1",
category: SuggestionCategory.dialogue,
suggestion:
'Peasant says: "You should go see the magistrate, he was mentioning he needed help."',
relevancyScore: 80,
},
{
id: "number2",
category: SuggestionCategory.monsters,
suggestion:
"A blast shakes the town. A Minotaur has blasted through the wall, and he's coming at you. Roll for initiation!",
relevancyScore: 85,
},
{
id: "number3",
category: SuggestionCategory.items,
suggestion: "You find a bag holding in the chest.",
relevancyScore: 60,
},
];
19 changes: 19 additions & 0 deletions api/types/suggestionTypes.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SuggestionCategory from "./enums";

export interface SuggestionObj {
id: string;
category: SuggestionCategory;
relevancyScore: number;
suggestion: string | null;
}

export interface SendPromptData {
id: string;
prompt: string;
mock?: boolean;
}

export interface AiResponseData {
id: string;
response: string | null;
}
7 changes: 7 additions & 0 deletions vite-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@formkit/auto-animate": "^0.8.2",
"@mui/icons-material": "^6.4.7",
"@mui/material": "^6.4.7",
"axios": "^1.7.9",
"lodash": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"socket.io-client": "^4.8.1",
Expand All @@ -26,6 +32,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"prettier": "^3.5.3",
"vite": "^6.1.0"
}
}
10 changes: 3 additions & 7 deletions vite-client/src/App.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
Expand Down Expand Up @@ -35,6 +28,9 @@

.card {
padding: 2em;
border-radius: 0.4em;
box-shadow: 5px 5px 14px rgba(0, 0, 0, 0.4);
background-color: #272b2b;
}

.read-the-docs {
Expand Down
124 changes: 17 additions & 107 deletions vite-client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,21 @@
import { useEffect, useState, useRef } from 'react';
import { io } from 'socket.io-client';
import useSpeechRecognition from './hooks/useSpeechRecognition';
import { v4 as uuidv4 } from 'uuid'; // For unique IDs

const socket = io('http://localhost:5000', { withCredentials: true });
import "./App.css";
import SuggestionsPage from "./pages/SuggestionsPage.jsx";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { theme } from "./theme.js";
import { SocketProvider } from "./providers/SocketProvider.jsx";
import { SuggestionsProvider } from "./providers/SuggestionsProvider.jsx";

function App() {
const [responses, setResponses] = useState([]);
const { transcript, listening, startListening, stopListening } = useSpeechRecognition();
const responseBoxRef = useRef(null);
const latencyStartTimes = useRef({}); // Store start times by request ID

useEffect(() => {
socket.on('ai_response', ({ id, response }) => {
const endTime = Date.now();
const startTime = latencyStartTimes.current[id] || endTime;
const latency = ((endTime - startTime) / 1000).toFixed(2);

setResponses((prevResponses) =>
prevResponses.map((entry) =>
entry.id === id ? { ...entry, response, latency } : entry
)
);

delete latencyStartTimes.current[id];
});

return () => socket.off('ai_response');
}, []);

useEffect(() => {
if (transcript.trim() !== '') {
const id = uuidv4(); // Unique ID for each request
latencyStartTimes.current[id] = Date.now(); // Start time per request

setResponses((prevResponses) => [
...prevResponses,
{ id, prompt: transcript, response: '', latency: null },
]);

socket.emit('send_prompt', { id, prompt: transcript });
}
}, [transcript]);

useEffect(() => {
// Auto-scroll when new responses arrive
if (responseBoxRef.current) {
responseBoxRef.current.scrollTop = responseBoxRef.current.scrollHeight;
}
}, [responses]);

return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center gap-6">
<h1 className="text-3xl font-bold">D&D AI Assistant Demo</h1>
<h2>(Browser Speech + WebSocket + OpenAI)</h2>
<button
onClick={listening ? stopListening : startListening}
className={`${
listening ? 'bg-red-500' : 'bg-green-500'
} text-white px-6 py-3 rounded text-lg`}
>
{listening ? 'Stop Listening 🛑' : 'Start Listening 🎙️'}
</button>

<div className="bg-white p-6 rounded shadow w-2/3">
<h2 className="text-xl font-semibold">Live Transcript:</h2>
<p className="bg-gray-100 p-4 rounded mt-2">{transcript || '🎤 Say something...'}</p>
</div>

{/* 🎯 FINAL SCROLLBOX FIX - Height limited + scrollable */}
<div
ref={responseBoxRef}
className="bg-green-100 p-6 rounded shadow w-2/3 border border-gray-300"
style={{
height: '400px', // Fixed height for the scrollbox
overflowY: 'auto', // Enables vertical scrolling
}}
>
<h2 className="text-xl font-semibold mb-4">AI Responses (Newest at Bottom):</h2>
<div className="flex flex-col gap-4">
{responses.length === 0 ? (
<p className="text-gray-500 text-center">AI responses will appear here.</p>
) : (
responses.map((entry) => (
<div key={entry.id} className="bg-white p-4 rounded shadow mb-2">
<p>
<strong>📝 Prompt:</strong> {entry.prompt || 'N/A'}
</p>
<p>
<strong>🤖 Response:</strong>{' '}
{entry.response || (
<span className="text-gray-400">Processing...</span>
)}
</p>
<p className="text-sm text-gray-500">
⏱️ <strong>Latency:</strong>{' '}
{entry.latency !== null
? `${entry.latency} seconds`
: 'Calculating...'}
</p>
</div>
))
)}
</div>
</div>
</div>
);
return (
<SocketProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
<SuggestionsProvider>
<SuggestionsPage />
</SuggestionsProvider>
</ThemeProvider>
</SocketProvider>
);
}

export default App;
export default App;
38 changes: 38 additions & 0 deletions vite-client/src/components/LoadingBars.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import {SvgIcon} from "@mui/material";

export function LoadingBarsIcon(props){
return (
<SvgIcon {...props}>
{/* credit: cog icon from https://heroicons.com */}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
<path transform="translate(2)" d="M0 12 V20 H4 V12z">
<animate attributeName="d" values="M0 12 V20 H4 V12z; M0 4 V28 H4 V4z; M0 12 V20 H4 V12z; M0 12 V20 H4 V12z"
dur="1.2s" repeatCount="indefinite" begin="0" keyTimes="0;.2;.5;1"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.8 0.4 0.8" calcMode="spline"/>
</path>
<path transform="translate(8)" d="M0 12 V20 H4 V12z">
<animate attributeName="d" values="M0 12 V20 H4 V12z; M0 4 V28 H4 V4z; M0 12 V20 H4 V12z; M0 12 V20 H4 V12z"
dur="1.2s" repeatCount="indefinite" begin="0.2" keyTimes="0;.2;.5;1"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.8 0.4 0.8" calcMode="spline"/>
</path>
<path transform="translate(14)" d="M0 12 V20 H4 V12z">
<animate attributeName="d" values="M0 12 V20 H4 V12z; M0 4 V28 H4 V4z; M0 12 V20 H4 V12z; M0 12 V20 H4 V12z"
dur="1.2s" repeatCount="indefinite" begin="0.4" keyTimes="0;.2;.5;1"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.8 0.4 0.8" calcMode="spline"/>
</path>
<path transform="translate(20)" d="M0 12 V20 H4 V12z">
<animate attributeName="d" values="M0 12 V20 H4 V12z; M0 4 V28 H4 V4z; M0 12 V20 H4 V12z; M0 12 V20 H4 V12z"
dur="1.2s" repeatCount="indefinite" begin="0.6" keyTimes="0;.2;.5;1"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.8 0.4 0.8" calcMode="spline"/>
</path>
<path transform="translate(26)" d="M0 12 V20 H4 V12z">
<animate attributeName="d" values="M0 12 V20 H4 V12z; M0 4 V28 H4 V4z; M0 12 V20 H4 V12z; M0 12 V20 H4 V12z"
dur="1.2s" repeatCount="indefinite" begin="0.8" keyTimes="0;.2;.5;1"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.8 0.4 0.8" calcMode="spline"/>
</path>
</svg>
</SvgIcon>
)

}
Loading