├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── api │ ├── functions │ │ ├── get_joke │ │ │ └── route.ts │ │ └── get_weather │ │ │ └── route.ts │ ├── turn_response │ │ └── route.ts │ └── vector_stores │ │ ├── add_file │ │ └── route.ts │ │ ├── create_store │ │ └── route.ts │ │ ├── list_files │ │ └── route.ts │ │ ├── retrieve_store │ │ └── route.ts │ │ └── upload_file │ │ └── route.ts ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── annotations.tsx ├── assistant.tsx ├── chat.tsx ├── country-selector.tsx ├── file-search-setup.tsx ├── file-upload.tsx ├── functions-view.tsx ├── message.tsx ├── panel-config.tsx ├── tool-call.tsx ├── tools-panel.tsx ├── ui │ ├── button.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── switch.tsx │ ├── textarea.tsx │ └── tooltip.tsx └── websearch-config.tsx ├── config ├── constants.ts ├── functions.ts └── tools-list.ts ├── eslint.config.mjs ├── lib ├── assistant.ts ├── tools │ ├── tools-handling.ts │ └── tools.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public └── openai_logo.svg ├── stores ├── useConversationStore.ts └── useToolsStore.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /.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 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /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 | 21 | --- 22 | 23 | Geist and Geist Mono Fonts 24 | 25 | Copyright (c) 2023 Vercel, in collaboration with basement.studio 26 | 27 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 28 | This license is copied below, and is also available with a FAQ at: 29 | http://scripts.sil.org/OFL 30 | 31 | ----------------------------------------------------------- 32 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 33 | ----------------------------------------------------------- 34 | 35 | PREAMBLE 36 | The goals of the Open Font License (OFL) are to stimulate worldwide 37 | development of collaborative font projects, to support the font creation 38 | efforts of academic and linguistic communities, and to provide a free and 39 | open framework in which fonts may be shared and improved in partnership 40 | with others. 41 | 42 | The OFL allows the licensed fonts to be used, studied, modified and 43 | redistributed freely as long as they are not sold by themselves. The 44 | fonts, including any derivative works, can be bundled, embedded, 45 | redistributed and/or sold with any software provided that any reserved 46 | names are not used by derivative works. The fonts and derivatives, 47 | however, cannot be released under any other type of license. The 48 | requirement for fonts to remain under this license does not apply 49 | to any document created using the fonts or their derivatives. 50 | 51 | DEFINITIONS 52 | "Font Software" refers to the set of files released by the Copyright 53 | Holder(s) under this license and clearly marked as such. This may 54 | include source files, build scripts and documentation. 55 | 56 | "Reserved Font Name" refers to any names specified as such after the 57 | copyright statement(s). 58 | 59 | "Original Version" refers to the collection of Font Software components as 60 | distributed by the Copyright Holder(s). 61 | 62 | "Modified Version" refers to any derivative made by adding to, deleting, 63 | or substituting -- in part or in whole -- any of the components of the 64 | Original Version, by changing formats or by porting the Font Software to a 65 | new environment. 66 | 67 | "Author" refers to any designer, engineer, programmer, technical 68 | writer or other person who contributed to the Font Software. 69 | 70 | PERMISSION AND CONDITIONS 71 | Permission is hereby granted, free of charge, to any person obtaining 72 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 73 | redistribute, and sell modified and unmodified copies of the Font 74 | Software, subject to the following conditions: 75 | 76 | 1) Neither the Font Software nor any of its individual components, 77 | in Original or Modified Versions, may be sold by itself. 78 | 79 | 2) Original or Modified Versions of the Font Software may be bundled, 80 | redistributed and/or sold with any software, provided that each copy 81 | contains the above copyright notice and this license. These can be 82 | included either as stand-alone text files, human-readable headers or 83 | in the appropriate machine-readable metadata fields within text or 84 | binary files as long as those fields can be easily viewed by the user. 85 | 86 | 3) No Modified Version of the Font Software may use the Reserved Font 87 | Name(s) unless explicit written permission is granted by the corresponding 88 | Copyright Holder. This restriction only applies to the primary font name as 89 | presented to the users. 90 | 91 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 92 | Software shall not be used to promote, endorse or advertise any 93 | Modified Version, except to acknowledge the contribution(s) of the 94 | Copyright Holder(s) and the Author(s) or with their explicit written 95 | permission. 96 | 97 | 5) The Font Software, modified or unmodified, in part or in whole, 98 | must be distributed entirely under this license, and must not be 99 | distributed under any other license. The requirement for fonts to 100 | remain under this license does not apply to any document created 101 | using the Font Software. 102 | 103 | TERMINATION 104 | This license becomes null and void if any of the above conditions are 105 | not met. 106 | 107 | DISCLAIMER 108 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 109 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 110 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 111 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 112 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 113 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 114 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 115 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 116 | OTHER DEALINGS IN THE FONT SOFTWARE. 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Responses starter app 2 | 3 | [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 4 | ![NextJS](https://img.shields.io/badge/Built_with-NextJS-blue) 5 | ![OpenAI API](https://img.shields.io/badge/Powered_by-OpenAI_API-orange) 6 | 7 | This repository contains a NextJS starter app built on top of the [Responses API](https://platform.openai.com/docs/api-reference/responses). 8 | It leverages built-in tools ([web search](https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses) and [file search](https://platform.openai.com/docs/guides/tools-file-search)) and implements a chat interface with multi-turn conversation handling. 9 | 10 | Features: 11 | 12 | - Multi-turn conversation handling 13 | - Web search tool configuration 14 | - Vector store creation & file upload for use with the file search tool 15 | - Function calling 16 | - Streaming responses & tool calls 17 | - Display annotations 18 | 19 | This app is meant to be used as a starting point to build a conversational assistant that you can customize to your needs. 20 | 21 | ## How to use 22 | 23 | 1. **Set up the OpenAI API:** 24 | 25 | - If you're new to the OpenAI API, [sign up for an account](https://platform.openai.com/signup). 26 | - Follow the [Quickstart](https://platform.openai.com/docs/quickstart) to retrieve your API key. 27 | 28 | 2. **Set the OpenAI API key:** 29 | 30 | 2 options: 31 | 32 | - Set the `OPENAI_API_KEY` environment variable [globally in your system](https://platform.openai.com/docs/libraries#create-and-export-an-api-key) 33 | - Set the `OPENAI_API_KEY` environment variable in the project: Create a `.env` file at the root of the project and add the following line (see `.env.example` for reference): 34 | 35 | ```bash 36 | OPENAI_API_KEY= 37 | ``` 38 | 39 | 3. **Clone the Repository:** 40 | 41 | ```bash 42 | git clone https://github.com/openai/openai-responses-starter-app.git 43 | ``` 44 | 45 | 4. **Install dependencies:** 46 | 47 | Run in the project root: 48 | 49 | ```bash 50 | npm install 51 | ``` 52 | 53 | 5. **Run the app:** 54 | 55 | ```bash 56 | npm run dev 57 | ``` 58 | 59 | The app will be available at [`http://localhost:3000`](http://localhost:3000). 60 | 61 | ## Contributing 62 | 63 | You are welcome to open issues or submit PRs to improve this app, however, please note that we may not review all suggestions. 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License. See the LICENSE file for details. 68 | -------------------------------------------------------------------------------- /app/api/functions/get_joke/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET() { 2 | try { 3 | // Fetch a programming joke 4 | const jokeRes = await fetch("https://v2.jokeapi.dev/joke/Programming"); 5 | if (!jokeRes.ok) throw new Error("Failed to fetch joke"); 6 | 7 | const jokeData = await jokeRes.json(); 8 | 9 | // Format joke response based on its type 10 | const joke = 11 | jokeData.type === "twopart" 12 | ? `${jokeData.setup} - ${jokeData.delivery}` 13 | : jokeData.joke; 14 | 15 | return new Response(JSON.stringify({ joke }), { status: 200 }); 16 | } catch (error) { 17 | console.error("Error fetching joke:", error); 18 | return new Response(JSON.stringify({ error: "Could not fetch joke" }), { 19 | status: 500, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/api/functions/get_weather/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | try { 3 | const { searchParams } = new URL(request.url); 4 | const location = searchParams.get("location"); 5 | const unit = searchParams.get("unit"); 6 | 7 | // 1. Get coordinates for the city 8 | const geoRes = await fetch( 9 | `https://nominatim.openstreetmap.org/search?q=${location}&format=json` 10 | ); 11 | const geoData = await geoRes.json(); 12 | 13 | if (!geoData.length) { 14 | return new Response(JSON.stringify({ error: "Invalid location" }), { 15 | status: 404, 16 | }); 17 | } 18 | 19 | const { lat, lon } = geoData[0]; 20 | 21 | // 2. Fetch weather data from Open-Meteo 22 | const weatherRes = await fetch( 23 | `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&hourly=temperature_2m&temperature_unit=${ 24 | unit ?? "celsius" 25 | }` 26 | ); 27 | 28 | if (!weatherRes.ok) { 29 | throw new Error("Failed to fetch weather data"); 30 | } 31 | 32 | const weather = await weatherRes.json(); 33 | 34 | // 3. Get current UTC time in ISO format 35 | const now = new Date(); 36 | const currentHourISO = now.toISOString().slice(0, 13) + ":00"; 37 | 38 | // 4. Get current temperature 39 | const index = weather.hourly.time.indexOf(currentHourISO); 40 | const currentTemperature = 41 | index !== -1 ? weather.hourly.temperature_2m[index] : null; 42 | 43 | if (currentTemperature === null) { 44 | return new Response( 45 | JSON.stringify({ error: "Temperature data unavailable" }), 46 | { status: 500 } 47 | ); 48 | } 49 | 50 | return new Response(JSON.stringify({ temperature: currentTemperature }), { 51 | status: 200, 52 | }); 53 | } catch (error) { 54 | console.error("Error getting weather:", error); 55 | return new Response(JSON.stringify({ error: "Error getting weather" }), { 56 | status: 500, 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/api/turn_response/route.ts: -------------------------------------------------------------------------------- 1 | import { MODEL } from "@/config/constants"; 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | export async function POST(request: Request) { 6 | try { 7 | const { messages, tools } = await request.json(); 8 | console.log("Received messages:", messages); 9 | 10 | const openai = new OpenAI(); 11 | 12 | const events = await openai.responses.create({ 13 | model: MODEL, 14 | input: messages, 15 | tools, 16 | stream: true, 17 | parallel_tool_calls: false, 18 | }); 19 | 20 | // Create a ReadableStream that emits SSE data 21 | const stream = new ReadableStream({ 22 | async start(controller) { 23 | try { 24 | for await (const event of events) { 25 | // Sending all events to the client 26 | const data = JSON.stringify({ 27 | event: event.type, 28 | data: event, 29 | }); 30 | controller.enqueue(`data: ${data}\n\n`); 31 | } 32 | // End of stream 33 | controller.close(); 34 | } catch (error) { 35 | console.error("Error in streaming loop:", error); 36 | controller.error(error); 37 | } 38 | }, 39 | }); 40 | 41 | // Return the ReadableStream as SSE 42 | return new Response(stream, { 43 | headers: { 44 | "Content-Type": "text/event-stream", 45 | "Cache-Control": "no-cache", 46 | }, 47 | }); 48 | } catch (error) { 49 | console.error("Error in POST handler:", error); 50 | return NextResponse.json( 51 | { 52 | error: error instanceof Error ? error.message : "Unknown error", 53 | }, 54 | { status: 500 } 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/api/vector_stores/add_file/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | const openai = new OpenAI(); 4 | 5 | export async function POST(request: Request) { 6 | const { vectorStoreId, fileId } = await request.json(); 7 | try { 8 | const vectorStore = await openai.vectorStores.files.create( 9 | vectorStoreId, 10 | { 11 | file_id: fileId, 12 | } 13 | ); 14 | return new Response(JSON.stringify(vectorStore), { status: 200 }); 15 | } catch (error) { 16 | console.error("Error adding file:", error); 17 | return new Response("Error adding file", { status: 500 }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/api/vector_stores/create_store/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | const openai = new OpenAI(); 4 | 5 | export async function POST(request: Request) { 6 | const { name } = await request.json(); 7 | try { 8 | const vectorStore = await openai.vectorStores.create({ 9 | name, 10 | }); 11 | return new Response(JSON.stringify(vectorStore), { status: 200 }); 12 | } catch (error) { 13 | console.error("Error creating vector store:", error); 14 | return new Response("Error creating vector store", { status: 500 }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/api/vector_stores/list_files/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | const openai = new OpenAI(); 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams } = new URL(request.url); 7 | const vectorStoreId = searchParams.get("vector_store_id"); 8 | 9 | try { 10 | const vectorStore = await openai.vectorStores.files.list( 11 | vectorStoreId || "" 12 | ); 13 | return new Response(JSON.stringify(vectorStore), { status: 200 }); 14 | } catch (error) { 15 | console.error("Error fetching files:", error); 16 | return new Response("Error fetching files", { status: 500 }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/api/vector_stores/retrieve_store/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | const openai = new OpenAI(); 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams } = new URL(request.url); 7 | const vectorStoreId = searchParams.get("vector_store_id"); 8 | try { 9 | const vectorStore = await openai.vectorStores.retrieve( 10 | vectorStoreId || "" 11 | ); 12 | return new Response(JSON.stringify(vectorStore), { status: 200 }); 13 | } catch (error) { 14 | console.error("Error fetching vector store:", error); 15 | return new Response("Error fetching vector store", { status: 500 }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/api/vector_stores/upload_file/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | const openai = new OpenAI(); 3 | 4 | export async function POST(request: Request) { 5 | const { fileObject } = await request.json(); 6 | 7 | try { 8 | const fileBuffer = Buffer.from(fileObject.content, "base64"); 9 | const fileBlob = new Blob([fileBuffer], { 10 | type: "application/octet-stream", 11 | }); 12 | 13 | const file = await openai.files.create({ 14 | file: new File([fileBlob], fileObject.name), 15 | purpose: "assistants", 16 | }); 17 | 18 | return new Response(JSON.stringify(file), { status: 200 }); 19 | } catch (error) { 20 | console.error("Error uploading file:", error); 21 | return new Response("Error uploading file", { status: 500 }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-responses-starter-app/bdab6faddab83f25aeeee2f66bf1b2db3d27cb08/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-responses-starter-app/bdab6faddab83f25aeeee2f66bf1b2db3d27cb08/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-responses-starter-app/bdab6faddab83f25aeeee2f66bf1b2db3d27cb08/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | 9 | --foreground: 224 71.4% 4.1%; 10 | 11 | --card: 0 0% 100%; 12 | 13 | --card-foreground: 224 71.4% 4.1%; 14 | 15 | --popover: 0 0% 100%; 16 | 17 | --popover-foreground: 224 71.4% 4.1%; 18 | 19 | --primary: 220.9 39.3% 11%; 20 | 21 | --primary-foreground: 210 20% 98%; 22 | 23 | --secondary: 220 14.3% 95.9%; 24 | 25 | --secondary-foreground: 220.9 39.3% 11%; 26 | 27 | --muted: 220 14.3% 95.9%; 28 | 29 | --muted-foreground: 220 8.9% 46.1%; 30 | 31 | --accent: 0 0% 98%; 32 | 33 | --accent-foreground: 220.9 39.3% 11%; 34 | 35 | --destructive: 0 84.2% 60.2%; 36 | 37 | --destructive-foreground: 210 20% 98%; 38 | 39 | --border: 220 13% 91%; 40 | 41 | --input: 220 13% 91%; 42 | 43 | --ring: 224 71.4% 4.1%; 44 | 45 | --chart-1: 12 76% 61%; 46 | 47 | --chart-2: 173 58% 39%; 48 | 49 | --chart-3: 197 37% 24%; 50 | 51 | --chart-4: 43 74% 66%; 52 | 53 | --chart-5: 27 87% 67%; 54 | 55 | --radius: 0.5rem; 56 | } 57 | .dark { 58 | --background: 224 71.4% 4.1%; 59 | 60 | --foreground: 210 20% 98%; 61 | 62 | --card: 224 71.4% 4.1%; 63 | 64 | --card-foreground: 210 20% 98%; 65 | 66 | --popover: 224 71.4% 4.1%; 67 | 68 | --popover-foreground: 210 20% 98%; 69 | 70 | --primary: 210 20% 98%; 71 | 72 | --primary-foreground: 220.9 39.3% 11%; 73 | 74 | --secondary: 215 27.9% 16.9%; 75 | 76 | --secondary-foreground: 210 20% 98%; 77 | 78 | --muted: 215 27.9% 16.9%; 79 | 80 | --muted-foreground: 217.9 10.6% 64.9%; 81 | 82 | --accent: 215 27.9% 16.9%; 83 | 84 | --accent-foreground: 210 20% 98%; 85 | 86 | --destructive: 0 62.8% 30.6%; 87 | 88 | --destructive-foreground: 210 20% 98%; 89 | 90 | --border: 215 27.9% 16.9%; 91 | 92 | --input: 215 27.9% 16.9%; 93 | 94 | --ring: 216 12.2% 83.9%; 95 | 96 | --chart-1: 220 70% 50%; 97 | 98 | --chart-2: 160 60% 45%; 99 | 100 | --chart-3: 30 80% 55%; 101 | 102 | --chart-4: 280 65% 60%; 103 | 104 | --chart-5: 340 75% 55%; 105 | } 106 | } 107 | 108 | @layer base { 109 | * { 110 | @apply border-border outline-ring/50; 111 | } 112 | body { 113 | @apply bg-background text-foreground; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Responses starter app", 18 | description: "Starter app for the OpenAI Responses API", 19 | icons: { 20 | icon: "/openai_logo.svg", 21 | }, 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 34 |
35 |
{children}
36 |
37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Assistant from "@/components/assistant"; 3 | import ToolsPanel from "@/components/tools-panel"; 4 | import { Menu, X } from "lucide-react"; 5 | import { useState } from "react"; 6 | 7 | export default function Main() { 8 | const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false); 9 | 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 | {/* Hamburger menu for small screens */} 19 |
20 | 23 |
24 | {/* Overlay panel for ToolsPanel on small screens */} 25 | {isToolsPanelOpen && ( 26 |
27 |
28 | 31 | 32 |
33 |
34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /components/annotations.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLinkIcon } from "lucide-react"; 2 | 3 | export type Annotation = { 4 | type: "file_citation" | "url_citation"; 5 | fileId?: string; 6 | url?: string; 7 | title?: string; 8 | filename?: string; 9 | index?: number; 10 | }; 11 | 12 | const AnnotationPill = ({ annotation }: { annotation: Annotation }) => { 13 | const className = 14 | "inline-block text-nowrap px-3 py-1 rounded-full text-xs max-w-48 shrink-0 text-ellipsis overflow-hidden bg-[#ededed] text-zinc-500"; 15 | 16 | switch (annotation.type) { 17 | case "file_citation": 18 | return {annotation.filename}; 19 | case "url_citation": 20 | return ( 21 | 27 |
28 |
{annotation.title}
29 | 30 |
31 |
32 | ); 33 | } 34 | }; 35 | 36 | const Annotations = ({ annotations }: { annotations: Annotation[] }) => { 37 | const uniqueAnnotations = annotations.reduce( 38 | (acc: Annotation[], annotation) => { 39 | if ( 40 | !acc.some( 41 | (a: Annotation) => 42 | a.type === annotation.type && 43 | ((annotation.type === "file_citation" && 44 | a.fileId === annotation.fileId) || 45 | (annotation.type === "url_citation" && a.url === annotation.url)) 46 | ) 47 | ) { 48 | acc.push(annotation); 49 | } 50 | return acc; 51 | }, 52 | [] 53 | ); 54 | 55 | return ( 56 |
57 | {uniqueAnnotations.map((annotation: Annotation, index: number) => ( 58 | 59 | ))} 60 |
61 | ); 62 | }; 63 | 64 | export default Annotations; 65 | -------------------------------------------------------------------------------- /components/assistant.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import Chat from "./chat"; 4 | import useConversationStore from "@/stores/useConversationStore"; 5 | import { Item, processMessages } from "@/lib/assistant"; 6 | 7 | export default function Assistant() { 8 | const { chatMessages, addConversationItem, addChatMessage } = 9 | useConversationStore(); 10 | 11 | const handleSendMessage = async (message: string) => { 12 | if (!message.trim()) return; 13 | 14 | const userItem: Item = { 15 | type: "message", 16 | role: "user", 17 | content: [{ type: "input_text", text: message.trim() }], 18 | }; 19 | const userMessage: any = { 20 | role: "user", 21 | content: message.trim(), 22 | }; 23 | 24 | try { 25 | addConversationItem(userMessage); 26 | addChatMessage(userItem); 27 | await processMessages(); 28 | } catch (error) { 29 | console.error("Error processing message:", error); 30 | } 31 | }; 32 | 33 | return ( 34 |
35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useCallback, useEffect, useRef, useState } from "react"; 4 | import ToolCall from "./tool-call"; 5 | import Message from "./message"; 6 | import Annotations from "./annotations"; 7 | import { Item } from "@/lib/assistant"; 8 | 9 | interface ChatProps { 10 | items: Item[]; 11 | onSendMessage: (message: string) => void; 12 | } 13 | 14 | const Chat: React.FC = ({ items, onSendMessage }) => { 15 | const itemsEndRef = useRef(null); 16 | const [inputMessageText, setinputMessageText] = useState(""); 17 | // This state is used to provide better user experience for non-English IMEs such as Japanese 18 | const [isComposing, setIsComposing] = useState(false); 19 | 20 | const scrollToBottom = () => { 21 | itemsEndRef.current?.scrollIntoView({ behavior: "instant" }); 22 | }; 23 | 24 | const handleKeyDown = useCallback((event: React.KeyboardEvent) => { 25 | if (event.key === "Enter" && !event.shiftKey && !isComposing) { 26 | event.preventDefault(); 27 | onSendMessage(inputMessageText); 28 | setinputMessageText(""); 29 | } 30 | }, [onSendMessage, inputMessageText]); 31 | 32 | useEffect(() => { 33 | scrollToBottom(); 34 | }, [items]); 35 | 36 | return ( 37 |
38 |
39 |
40 |
41 | {items.map((item, index) => ( 42 | 43 | {item.type === "tool_call" ? ( 44 | 45 | ) : item.type === "message" ? ( 46 |
47 | 48 | {item.content && 49 | item.content[0].annotations && 50 | item.content[0].annotations.length > 0 && ( 51 | 54 | )} 55 |
56 | ) : null} 57 |
58 | ))} 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |