├── server ├── app │ ├── __init__.py │ ├── agent_config.py │ ├── mock_api.py │ └── utils.py ├── .python-version ├── pyproject.toml ├── server.py └── uv.lock ├── frontend ├── postcss.config.mjs ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── styles.css │ │ ├── layout.tsx │ │ ├── globals.css │ │ └── page.tsx │ ├── components │ │ ├── ui │ │ │ ├── utils.ts │ │ │ └── Button.tsx │ │ ├── icons │ │ │ ├── PauseIcon.tsx │ │ │ ├── ArrowUpIcon.tsx │ │ │ ├── ClockIcon.tsx │ │ │ ├── MicIcon.tsx │ │ │ ├── WebSearchIcon.tsx │ │ │ ├── ShuffleIcon.tsx │ │ │ ├── WriteIcon.tsx │ │ │ └── FunctionsIcon.tsx │ │ ├── AudioPlayback.tsx │ │ ├── messages │ │ │ ├── WebSearchMessage.tsx │ │ │ ├── HandoffMessage.tsx │ │ │ ├── TextMessage.tsx │ │ │ └── FunctionCallMessage.tsx │ │ ├── ChatLoadingDots.tsx │ │ ├── MessageBubble.tsx │ │ ├── Composer.tsx │ │ ├── ChatDialog.tsx │ │ ├── AudioChat.tsx │ │ └── Header.tsx │ ├── lib │ │ ├── types.ts │ │ └── utils.ts │ └── hooks │ │ ├── useAudio.ts │ │ └── useWebsocket.ts ├── next.config.ts ├── tailwind.config.mjs ├── eslint.config.mjs ├── tsconfig.json ├── package.json └── public │ ├── logo.svg │ └── openai_logo.svg ├── Makefile ├── .gitignore ├── LICENSE └── README.md /server/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-voice-agent-sdk-sample/HEAD/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: sync 2 | sync: 3 | cd frontend && npm install 4 | cd server && uv sync 5 | 6 | 7 | .PHONY: serve 8 | serve: 9 | cd frontend && npm run dev 10 | 11 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | devIndicators: false, 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /frontend/src/components/ui/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | theme: { 3 | fontFamily: { 4 | sans: ['"SF Pro text"', "system-ui", "sans-serif"], 5 | serif: ['"New York"', "serif"], 6 | }, 7 | }, 8 | plugins: [], 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /frontend/src/app/styles.css: -------------------------------------------------------------------------------- 1 | @keyframes contentShow { 2 | from { 3 | transform: translateY(100%); 4 | } 5 | to { 6 | transform: translateY(0); 7 | } 8 | } 9 | 10 | @keyframes overlayShow { 11 | from { 12 | opacity: 0; 13 | } 14 | to { 15 | opacity: 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResponseFunctionToolCall, 3 | ResponseInputItem, 4 | ResponseOutputItem, 5 | } from "openai/resources/responses/responses.mjs"; 6 | 7 | export type ToolCall = ResponseFunctionToolCall & { output?: string }; 8 | export type Message = ResponseInputItem | ResponseOutputItem | ToolCall; 9 | -------------------------------------------------------------------------------- /server/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "openai-voice-agents-sdk-sample" 3 | version = "0.1.0" 4 | description = "Example python backend server for the Agents SDK" 5 | readme = "../README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "fastapi[standard]>=0.115.11", 9 | "numpy>=2.2.3", 10 | "openai>=1.68.2", 11 | "openai-agents[voice]>=0.0.6", 12 | "python-dotenv>=1.0.1", 13 | "uvicorn>=0.34.0", 14 | ] 15 | -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /frontend/src/components/icons/PauseIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const PauseIcon = (props: React.SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | 16 | export default PauseIcon; 17 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Voice agent sample app", 6 | description: 7 | "Starter app for building voice agents using the OpenAI Agents SDK", 8 | icons: { 9 | icon: "/openai_logo.svg", 10 | }, 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode; 17 | }>) { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ArrowUpIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const ArrowUpIcon = (props: React.SVGProps) => ( 4 | 12 | 17 | 18 | ); 19 | 20 | export default ArrowUpIcon; 21 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ClockIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const ClockIcon = (props: React.SVGProps) => ( 4 | 12 | 17 | 18 | ); 19 | 20 | export default ClockIcon; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | .next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | __pycache__ 44 | .venv/ 45 | 46 | 47 | dist/ -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/icons/MicIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MicIcon = (props: React.SVGProps) => ( 4 | 12 | 17 | 18 | 19 | ); 20 | 21 | export default MicIcon; 22 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #000000; 6 | --font-serif: "New York", serif; 7 | --font-sans: "SF Pro text", system-ui, sans-serif; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | background: var(--background); 19 | color: var(--foreground); 20 | } 21 | 22 | p { 23 | margin-top: 0.5rem; 24 | margin-bottom: 0.5rem; 25 | } 26 | 27 | ul { 28 | list-style-type: disc; 29 | padding-left: 1.25rem; /* Adjust as needed */ 30 | } 31 | ol { 32 | list-style-type: decimal; 33 | padding-left: 1.25rem; /* Adjust as needed */ 34 | } 35 | 36 | li { 37 | padding-top: 0.25rem; 38 | padding-bottom: 0.25rem; 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/AudioPlayback.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "motion/react"; 2 | 3 | import { cn } from "@/components/ui/utils"; 4 | 5 | export function AudioPlayback({ 6 | playbackFrequencies, 7 | itemClassName, 8 | className, 9 | height = 36, 10 | }: { 11 | playbackFrequencies: number[]; 12 | itemClassName?: string; 13 | className?: string; 14 | height?: number; 15 | }) { 16 | return ( 17 |
20 | {playbackFrequencies.map((frequency: number, index: number) => ( 21 | 28 | ))} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 OpenAI 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /frontend/src/components/messages/WebSearchMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ResponseFunctionWebSearch } from "openai/resources/responses/responses.mjs"; 2 | import React from "react"; 3 | 4 | import WebSearchIcon from "@/components/icons/WebSearchIcon"; 5 | 6 | type WebSearchMessageProps = { 7 | message: ResponseFunctionWebSearch; 8 | }; 9 | 10 | export function WebSearchMessage({ message }: WebSearchMessageProps) { 11 | return ( 12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 | {message.status === "completed" 20 | ? "Web search completed" 21 | : `Searching the web...`} 22 |
23 |
24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/components/ChatLoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "motion/react"; 3 | 4 | export default function ChatLoadingDots() { 5 | return ( 6 |
11 | 16 | 26 | 36 | Loading... 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/messages/HandoffMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ShuffleIcon from "@/components/icons/ShuffleIcon"; 4 | import { ToolCall } from "@/lib/types"; 5 | 6 | type HandoffMessageProps = { 7 | message: ToolCall; 8 | }; 9 | 10 | export function HandoffMessage({ message }: HandoffMessageProps) { 11 | let agentName: string; 12 | if (message?.output) { 13 | agentName = message?.output?.match(/'assistant':\s*'([^']+)'/)?.[1] || ""; 14 | } else { 15 | agentName = message.name; 16 | } 17 | 18 | return ( 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 | {message.status === "completed" 27 | ? `Transferred to ${agentName}` 28 | : `Transferring conversation...`} 29 |
30 |
31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/messages/TextMessage.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | import ReactMarkdown from "react-markdown"; 4 | 5 | type CustomLinkProps = { 6 | href?: string; 7 | children?: React.ReactNode; 8 | }; 9 | 10 | const CustomLink = ({ href, children, ...props }: CustomLinkProps) => ( 11 | 16 | {children} 17 | 18 | ); 19 | 20 | type TextMessageProps = { 21 | text: string; 22 | isUser: boolean; 23 | }; 24 | 25 | export function TextMessage({ text, isUser }: TextMessageProps) { 26 | return ( 27 |
32 |
38 | {text} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/icons/WebSearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const WebSearchIcon = (props: React.SVGProps) => ( 4 | 12 | 17 | 18 | ); 19 | 20 | export default WebSearchIcon; 21 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ShuffleIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const ShuffleIcon = (props: React.SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | 16 | export default ShuffleIcon; 17 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-voice-agent-sdk-sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev:next": "next dev --turbo", 7 | "dev:server": "cd ../server && uv run server.py", 8 | "dev": "concurrently \"npm run dev:next\" \"npm run dev:server\"", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@radix-ui/react-slot": "^1.1.2", 15 | "class-variance-authority": "^0.7.1", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.484.0", 18 | "motion": "^12.4.10", 19 | "next": "^15.2.4", 20 | "openai": "^4.87.3", 21 | "react": "^19.0.0", 22 | "react-dom": "^19.0.0", 23 | "react-markdown": "^10.1.0", 24 | "react-syntax-highlighter": "^15.6.1", 25 | "tailwind-merge": "^3.0.2", 26 | "wavtools": "^0.1.5" 27 | }, 28 | "devDependencies": { 29 | "@eslint/eslintrc": "^3", 30 | "@tailwindcss/postcss": "^4", 31 | "@types/node": "^20", 32 | "@types/react": "^19", 33 | "@types/react-dom": "^19", 34 | "@types/react-syntax-highlighter": "^15.5.13", 35 | "concurrently": "^9.1.2", 36 | "eslint": "^9", 37 | "eslint-config-next": "15.2.1", 38 | "tailwindcss": "^4", 39 | "typescript": "^5" 40 | }, 41 | "optionalDependencies": { 42 | "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", 43 | "lightningcss-linux-x64-gnu": "^1.29.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/app/agent_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from agents import Agent, WebSearchTool, function_tool 4 | from agents.tool import UserLocation 5 | 6 | import app.mock_api as mock_api 7 | 8 | STYLE_INSTRUCTIONS = "Use a conversational tone and write in a chat style without formal formatting or lists and do not use any emojis." 9 | 10 | 11 | @function_tool 12 | def get_past_orders(): 13 | return json.dumps(mock_api.get_past_orders()) 14 | 15 | 16 | @function_tool 17 | def submit_refund_request(order_number: str): 18 | """Confirm with the user first""" 19 | return mock_api.submit_refund_request(order_number) 20 | 21 | 22 | customer_support_agent = Agent( 23 | name="Customer Support Agent", 24 | instructions=f"You are a customer support assistant. {STYLE_INSTRUCTIONS}", 25 | model="gpt-4o-mini", 26 | tools=[get_past_orders, submit_refund_request], 27 | ) 28 | 29 | stylist_agent = Agent( 30 | name="Stylist Agent", 31 | model="gpt-4o-mini", 32 | instructions=f"You are a stylist assistant. {STYLE_INSTRUCTIONS}", 33 | tools=[WebSearchTool(user_location=UserLocation(type="approximate", city="Tokyo"))], 34 | handoffs=[customer_support_agent], 35 | ) 36 | 37 | triage_agent = Agent( 38 | name="Triage Agent", 39 | model="gpt-4o-mini", 40 | instructions=f"Route the user to the appropriate agent based on their request. {STYLE_INSTRUCTIONS}", 41 | handoffs=[stylist_agent, customer_support_agent], 42 | ) 43 | 44 | starting_agent = triage_agent 45 | -------------------------------------------------------------------------------- /frontend/src/components/icons/WriteIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const WriteIcon = (props: React.SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | 19 | export default WriteIcon; 20 | -------------------------------------------------------------------------------- /frontend/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/components/MessageBubble.tsx: -------------------------------------------------------------------------------- 1 | import { ResponseFunctionWebSearch } from "openai/resources/responses/responses.mjs"; 2 | import React from "react"; 3 | 4 | import { FunctionCallMessage } from "@/components/messages/FunctionCallMessage"; 5 | import { TextMessage } from "@/components/messages/TextMessage"; 6 | import { WebSearchMessage } from "@/components/messages/WebSearchMessage"; 7 | import { Message } from "@/lib/types"; 8 | 9 | type MessageBubbleProps = { 10 | message: Message; 11 | }; 12 | 13 | export function MessageBubble({ message }: MessageBubbleProps) { 14 | switch (message.type) { 15 | case "function_call": 16 | return ; 17 | case "function_call_output": 18 | // already rendered in FunctionCall 19 | return null; 20 | case "file_search_call": 21 | return ( 22 |
23 | file_search_call: {message.queries.join(", ")} 24 |
25 | ); 26 | case "message": 27 | if (Array.isArray(message.content)) { 28 | const content = message.content[0]; 29 | if (content.type === "output_text") { 30 | const isUser = message.role === "user"; 31 | return ; 32 | } else if (content.type === "refusal") { 33 | return null; 34 | } 35 | } else if (typeof message.content === "string") { 36 | const isUser = message.role === "user"; 37 | return ; 38 | } 39 | return null; 40 | case "web_search_call": 41 | return ( 42 | 43 | ); 44 | default: 45 | console.log("Unknown message type", message); 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import AudioChat from "@/components/AudioChat"; 4 | import { ChatHistory } from "@/components/ChatDialog"; 5 | import { Composer } from "@/components/Composer"; 6 | import { Header } from "@/components/Header"; 7 | import { useAudio } from "@/hooks/useAudio"; 8 | import { useWebsocket } from "@/hooks/useWebsocket"; 9 | import { useState } from "react"; 10 | 11 | import "./styles.css"; 12 | 13 | export default function Home() { 14 | const [prompt, setPrompt] = useState(""); 15 | 16 | const { 17 | isReady: audioIsReady, 18 | playAudio, 19 | startRecording, 20 | stopRecording, 21 | stopPlaying, 22 | frequencies, 23 | playbackFrequencies, 24 | } = useAudio(); 25 | const { 26 | isReady: websocketReady, 27 | sendAudioMessage, 28 | sendTextMessage, 29 | history: messages, 30 | resetHistory, 31 | isLoading, 32 | agentName, 33 | } = useWebsocket({ 34 | onNewAudio: playAudio, 35 | }); 36 | 37 | function handleSubmit() { 38 | setPrompt(""); 39 | sendTextMessage(prompt); 40 | } 41 | 42 | async function handleStopPlaying() { 43 | await stopPlaying(); 44 | } 45 | 46 | return ( 47 |
48 |
54 | 55 | 68 | } 69 | /> 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/components/icons/FunctionsIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const FunctionsIcon = (props: React.SVGProps) => ( 4 | 10 | 15 | 16 | ); 17 | 18 | export default FunctionsIcon; 19 | -------------------------------------------------------------------------------- /frontend/src/components/Composer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | import ArrowUpIcon from "@/components/icons/ArrowUpIcon"; 4 | import { Button } from "@/components/ui/Button"; 5 | 6 | interface ComposerProps { 7 | prompt: string; 8 | setPrompt: (prompt: string) => void; 9 | onSubmit: () => void; 10 | isLoading: boolean; 11 | audioChat?: React.ReactNode; 12 | } 13 | 14 | export function Composer({ 15 | prompt, 16 | setPrompt, 17 | onSubmit, 18 | isLoading, 19 | audioChat, 20 | }: ComposerProps) { 21 | const textareaRef = useRef(null); 22 | 23 | useEffect(() => { 24 | if (textareaRef.current) { 25 | textareaRef.current.style.height = "auto"; 26 | textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; 27 | } 28 | }, [prompt]); 29 | 30 | return ( 31 |
32 |
33 |