├── backend ├── app │ ├── __init__.py │ ├── data │ │ ├── __init__.py │ │ ├── gemini_streaming_ai.py │ │ └── openai_travel_agent_call.py │ ├── test │ │ └── __init__.py │ ├── web │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ ├── save_chat_db.cpython-312.pyc │ │ │ ├── travel_agent_call.cpython-312.pyc │ │ │ ├── gemini_streaming_ai.cpython-312.pyc │ │ │ └── openai_travel_agent_call.cpython-312.pyc │ │ ├── save_chat_db.py │ │ ├── openai_travel_agent_call.py │ │ └── gemini_streaming_ai.py │ ├── model │ │ ├── __init__.py │ │ ├── gemini_streaming_ai.py │ │ ├── openai_travel_agent_call.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ └── save_chat_db.cpython-312.pyc │ │ └── save_chat_db.py │ ├── service │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ ├── save_chat_db.cpython-312.pyc │ │ │ ├── travel_agent_call.cpython-312.pyc │ │ │ ├── gemini_streaming_ai.cpython-312.pyc │ │ │ └── openai_travel_agent_call.cpython-312.pyc │ │ ├── openai_travel_agent_call.py │ │ ├── save_chat_db.py │ │ └── gemini_streaming_ai.py │ ├── utils │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ ├── add_markers.cpython-312.pyc │ │ │ ├── update_map.cpython-312.pyc │ │ │ ├── chat_assistant.cpython-312.pyc │ │ │ ├── get_assistant.cpython-312.pyc │ │ │ ├── seed_assistant.cpython-312.pyc │ │ │ └── thread_manager.cpython-312.pyc │ │ ├── thread_manager.py │ │ ├── seed_assistant.py │ │ ├── update_map.py │ │ ├── add_markers.py │ │ ├── get_assistant.py │ │ ├── files.py │ │ └── chat_assistant.py │ ├── __pycache__ │ │ ├── main.cpython-312.pyc │ │ └── __init__.cpython-312.pyc │ └── main.py ├── .env.template ├── .dockerignore ├── requirements.txt └── Dockerfile ├── .gitignore ├── nextjs-frontend ├── .dockerignore ├── .eslintrc.json ├── src │ ├── app │ │ ├── globals.css │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── ui │ │ │ ├── gemini │ │ │ │ ├── ChatUi.tsx │ │ │ │ └── GeminiChatBox.tsx │ │ │ ├── openai │ │ │ │ ├── ChatMessage.tsx │ │ │ │ ├── MapAIServiceAssistant.tsx │ │ │ │ └── ChatBox.tsx │ │ │ └── AnimatedHero.tsx │ │ ├── api │ │ │ ├── gemini-mapstate │ │ │ │ └── route.ts │ │ │ ├── streaming-gemini │ │ │ │ └── route.ts │ │ │ └── sendMessage │ │ │ │ └── route.ts │ │ └── page.tsx │ ├── lib │ │ └── utils.ts │ ├── hooks │ │ ├── UseEscapePressa.tsx │ │ └── TogglePageOverflow.tsx │ ├── components │ │ ├── BottomBar.tsx │ │ ├── ui │ │ │ ├── input.tsx │ │ │ ├── tabs.tsx │ │ │ └── button.tsx │ │ ├── GoogleMapComponent.tsx │ │ ├── Visuals.tsx │ │ ├── FeatureTitle.tsx │ │ ├── Icons.tsx │ │ └── AnimatedCards.tsx │ └── store │ │ └── FeatureStore.tsx ├── .env.example ├── public │ ├── ai.png │ ├── down.png │ ├── human.png │ ├── vercel.svg │ └── next.svg ├── next.config.js ├── postcss.config.js ├── components.json ├── .gitignore ├── Dockerfile ├── tsconfig.json ├── tailwind.config.ts ├── package.json └── README.md ├── streamlit ├── requirements.txt ├── .env.example ├── app.py └── sample.py ├── LICENSE.md └── README.md /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | travel-ai-gauth.json -------------------------------------------------------------------------------- /backend/app/data/gemini_streaming_ai.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/model/gemini_streaming_ai.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/data/openai_travel_agent_call.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/model/openai_travel_agent_call.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs-frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | .next 4 | .env -------------------------------------------------------------------------------- /backend/.env.template: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TRAVEL_ASSISTANT_ID= 3 | DB_CONN_STR= -------------------------------------------------------------------------------- /nextjs-frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /streamlit/requirements.txt: -------------------------------------------------------------------------------- 1 | plotly 2 | requests 3 | streamlit 4 | python-dotenv 5 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | .next 4 | .env 5 | travel-ai-gauth.json -------------------------------------------------------------------------------- /nextjs-frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /nextjs-frontend/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BACKEND_API_URL= 2 | NEXT_PUBLIC_GOOGLE_MAPS_KEY= 3 | NEXT_PUBLIC_FRONTEND_API_URL= -------------------------------------------------------------------------------- /nextjs-frontend/public/ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/nextjs-frontend/public/ai.png -------------------------------------------------------------------------------- /nextjs-frontend/public/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/nextjs-frontend/public/down.png -------------------------------------------------------------------------------- /nextjs-frontend/public/human.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/nextjs-frontend/public/human.png -------------------------------------------------------------------------------- /nextjs-frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/nextjs-frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /nextjs-frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | openai 3 | requests 4 | uvicorn 5 | python-dotenv 6 | SQLAlchemy 7 | psycopg2 8 | google-cloud-aiplatform -------------------------------------------------------------------------------- /nextjs-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /backend/app/__pycache__/main.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/__pycache__/main.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/web/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/web/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /streamlit/.env.example: -------------------------------------------------------------------------------- 1 | MAPBOX_TOKEN="" # option # use it for maps styling if needed 2 | BACKEND_API_URL="your localhost or cloud backend url i.e http://localhost:8000" -------------------------------------------------------------------------------- /backend/app/model/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/model/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/service/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/service/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/add_markers.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/add_markers.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/update_map.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/update_map.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/web/__pycache__/save_chat_db.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/web/__pycache__/save_chat_db.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/model/__pycache__/save_chat_db.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/model/__pycache__/save_chat_db.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/service/__pycache__/save_chat_db.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/service/__pycache__/save_chat_db.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/chat_assistant.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/chat_assistant.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/get_assistant.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/get_assistant.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/seed_assistant.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/seed_assistant.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/utils/__pycache__/thread_manager.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/utils/__pycache__/thread_manager.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/web/__pycache__/travel_agent_call.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/web/__pycache__/travel_agent_call.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/service/__pycache__/travel_agent_call.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/service/__pycache__/travel_agent_call.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/web/__pycache__/gemini_streaming_ai.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/web/__pycache__/gemini_streaming_ai.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/service/__pycache__/gemini_streaming_ai.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/service/__pycache__/gemini_streaming_ai.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/web/__pycache__/openai_travel_agent_call.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/web/__pycache__/openai_travel_agent_call.cpython-312.pyc -------------------------------------------------------------------------------- /backend/app/service/__pycache__/openai_travel_agent_call.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjunaidca/travel-ai-service/HEAD/backend/app/service/__pycache__/openai_travel_agent_call.cpython-312.pyc -------------------------------------------------------------------------------- /nextjs-frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /travel_ai 4 | 5 | COPY ./requirements.txt /travel_ai/requirements.txt 6 | 7 | RUN pip install --no-cache-dir --upgrade -r /travel_ai/requirements.txt 8 | 9 | COPY ./app /travel_ai/app 10 | 11 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] 12 | -------------------------------------------------------------------------------- /nextjs-frontend/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": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /nextjs-frontend/src/hooks/UseEscapePressa.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useEscapePress = (onEscapePress: () => void) => { 4 | useEffect(() => { 5 | const onKeyUp = (ev: KeyboardEvent) => { 6 | if (ev.key === "Escape") onEscapePress(); 7 | }; 8 | 9 | window.addEventListener("keyup", onKeyUp); 10 | 11 | return () => { 12 | window.removeEventListener("keyup", onKeyUp); 13 | }; 14 | }, [onEscapePress]); 15 | }; 16 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from fastapi import FastAPI 4 | 5 | from .web import openai_travel_agent_call 6 | from .web import save_chat_db 7 | from .web import gemini_streaming_ai 8 | 9 | app = FastAPI() 10 | app.include_router(openai_travel_agent_call.router) 11 | app.include_router(save_chat_db.router) 12 | app.include_router(gemini_streaming_ai.router) 13 | 14 | 15 | @app.get("/") 16 | def top(): 17 | return "top here" 18 | # test it: http localhost:8000 19 | 20 | 21 | if __name__ == "__main__": 22 | uvicorn.run("main:app", reload=True) 23 | -------------------------------------------------------------------------------- /nextjs-frontend/.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /nextjs-frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/model/save_chat_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 2 | 3 | """ 4 | table user_interaction 5 | - thread_id str pk 6 | - last_prompt str 7 | - thread_message 8 | """ 9 | 10 | 11 | class Base(DeclarativeBase): 12 | pass 13 | 14 | 15 | class UserInteraction(Base): 16 | __tablename__ = 'user_interaction' 17 | 18 | thread_id: Mapped[str] = mapped_column( 19 | primary_key=True 20 | ) 21 | last_prompt: Mapped[str] = mapped_column( 22 | nullable=False 23 | ) 24 | thread_message: Mapped[str] = mapped_column( 25 | nullable=False, 26 | ) 27 | -------------------------------------------------------------------------------- /nextjs-frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the desired base image 2 | FROM node:18-alpine 3 | 4 | # Set the working directory in the container 5 | WORKDIR /nextjs_travel_ai_ui 6 | 7 | # Install pnpm 8 | RUN npm install -g pnpm 9 | 10 | # Copy package.json and optionally other configuration files (like pnpm-lock.yaml) 11 | COPY package.json pnpm-lock.yaml* ./ 12 | 13 | # Install dependencies 14 | RUN pnpm install 15 | 16 | # Copy the rest of your application's source code 17 | COPY . . 18 | 19 | # Build the Next.js application using the locally installed next 20 | RUN pnpm build 21 | 22 | # Your application's start command 23 | CMD ["pnpm", "start"] 24 | -------------------------------------------------------------------------------- /backend/app/web/save_chat_db.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from ..service.save_chat_db import save_chat_db 3 | 4 | router = APIRouter(prefix="/save_chat") 5 | 6 | 7 | @router.post("/") 8 | def save_thread_to_db(last_prompt: str, thread_id: str, thread_message): 9 | try: 10 | # Use the validated prompt from Pydantic model 11 | response = save_chat_db( 12 | last_prompt=last_prompt, thread_id=thread_id, thread_message=thread_message) 13 | print('response', response) 14 | 15 | return response 16 | except Exception as e: 17 | print("Validation error:", e) # Debugging 18 | return f"error, str(e)" 19 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Travel AI Assistant", 10 | description: "An OpenAI Assistant Microservice Frontend", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/utils/thread_manager.py: -------------------------------------------------------------------------------- 1 | from openai.types.beta.thread import Thread 2 | from openai import OpenAI 3 | 4 | 5 | class CreateThread(): 6 | def __init__(self, client: OpenAI): 7 | if client is None: 8 | raise Exception("OpenAI Client is not initialized") 9 | self.client = client 10 | print("CreateThread initialized with OpenAI client.") 11 | 12 | def create_thread(self, purpose: str = 'assistants') -> Thread: 13 | """Create a Thread.""" 14 | print(f"Creating a new Thread for purpose: '{purpose}'...") 15 | thread: Thread = self.client.beta.threads.create() 16 | print(f"New Thread created. Thread ID: {thread.id}") 17 | return thread 18 | -------------------------------------------------------------------------------- /nextjs-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 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 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/ui/gemini/ChatUi.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import HUMAN from "/public/human.png"; 3 | import AIIMAGE from "/public/ai.png"; 4 | 5 | const UiMessage = ({ sender, message }: { sender: any; message: any }) => { 6 | return ( 7 |
10 |
11 | {sender} 16 |
17 |

{message}

18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default UiMessage; 25 | -------------------------------------------------------------------------------- /backend/app/web/openai_travel_agent_call.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from ..service.openai_travel_agent_call import call_travel_assistant 3 | from ..utils.add_markers import markers_state 4 | from ..utils.update_map import map_state 5 | 6 | router = APIRouter(prefix="/travel_assistant") 7 | 8 | 9 | @router.post("/") 10 | def openai_travel_assistant(prompt: str): 11 | try: 12 | # Use the validated prompt from Pydantic model 13 | response, thread_id = call_travel_assistant(prompt) 14 | print('thread_id', thread_id) 15 | 16 | return { 17 | "markers_state": markers_state, 18 | "map_state": map_state, 19 | "openai_response": response 20 | } 21 | except Exception as e: 22 | print("Validation error:", e) # Debugging 23 | return {"error": str(e)} 24 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/BottomBar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const BottomBar = () => { 4 | return ( 5 |
6 |
7 |

8 | Copyright © 2023 mjunaidca 9 |

10 |
11 |
12 |

13 | Code by.{" "} 14 | 15 | {" "} 16 | mjunaidca on github{" "} 17 | 18 |

19 |
20 |
21 | ); 22 | }; 23 | 24 | export default BottomBar; 25 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/api/gemini-mapstate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export const runtime = "edge"; 4 | 5 | export async function GET(request: NextRequest) { 6 | try { 7 | const response = await fetch( 8 | `${process.env.NEXT_PUBLIC_BACKEND_API_URL}/gemini_streaming_travel_ai/mapstate`, 9 | { 10 | cache: "no-store", 11 | } 12 | ); 13 | 14 | if (!response.ok) { 15 | return new NextResponse( 16 | `API request failed with status ${response.status}`, 17 | { status: response.status } 18 | ); 19 | } 20 | 21 | const data = await response.json(); 22 | 23 | console.log(data); 24 | 25 | return NextResponse.json(data, { status: 200 }); 26 | } catch (error) { 27 | console.error("Fetch error:", error); 28 | return new NextResponse("Internal Server Error", { status: 500 }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nextjs-frontend/src/store/FeatureStore.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type FeaturesStore = { 4 | inViewFeature: string | null; 5 | setInViewFeature: (feature: string | null) => void; 6 | fullscreenFeature: string | null; 7 | setFullscreenFeature: (feature: string | null) => void; 8 | lastFullscreenFeature: string | null; 9 | setLastFullscreenFeature: (feature: string | null) => void; 10 | }; 11 | 12 | export const useFeatureStore = create((set) => ({ 13 | inViewFeature: null, 14 | setInViewFeature: (feature: string | null) => set({ inViewFeature: feature }), 15 | fullscreenFeature: null, 16 | setFullscreenFeature: (feature: string | null) => { 17 | set({ fullscreenFeature: feature }); 18 | if (feature !== null) { 19 | set({ lastFullscreenFeature: feature }); 20 | } 21 | }, 22 | lastFullscreenFeature: null, 23 | setLastFullscreenFeature: (feature: string | null) => 24 | set({ lastFullscreenFeature: feature }), 25 | })); 26 | -------------------------------------------------------------------------------- /backend/app/web/gemini_streaming_ai.py: -------------------------------------------------------------------------------- 1 | from fastapi.responses import StreamingResponse 2 | from fastapi import APIRouter, HTTPException 3 | from ..service.gemini_streaming_ai import ai_powered_map, call_gemini_travel_assistant 4 | 5 | router = APIRouter(prefix="/gemini_streaming_travel_ai") 6 | 7 | 8 | @router.get('/') 9 | async def stream(query: str): 10 | print("query", query) 11 | # no-store directive instructs response must not be stored in any cache 12 | headers = {"Cache-Control": "no-store"} 13 | try: 14 | return StreamingResponse(call_gemini_travel_assistant(query), media_type="text/event-stream", headers=headers) 15 | except Exception as e: 16 | # Log the error or take other appropriate actions 17 | raise HTTPException(status_code=500, detail=str(e)) 18 | 19 | 20 | @router.get("/mapstate") 21 | def get_latest_map_state(): 22 | return { 23 | "markers_state": ai_powered_map['markers_state'], 24 | "map_state": ai_powered_map['map_state'] 25 | } 26 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /nextjs-frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | keyframes: { 20 | "accordion-down": { 21 | from: { height: 0 }, 22 | to: { height: "var(--radix-accordion-content-height)" }, 23 | }, 24 | "accordion-up": { 25 | from: { height: "var(--radix-accordion-content-height)" }, 26 | to: { height: 0 }, 27 | }, 28 | }, 29 | animation: { 30 | "accordion-down": "accordion-down 0.2s ease-out", 31 | "accordion-up": "accordion-up 0.2s ease-out", 32 | }, 33 | }, 34 | }, 35 | plugins: [require("tailwindcss-animate")], 36 | } -------------------------------------------------------------------------------- /nextjs-frontend/src/app/api/streaming-gemini/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export const runtime = "edge"; 4 | 5 | export async function GET(request: NextRequest) { 6 | const { searchParams } = new URL(request.url); 7 | const message = searchParams.get("query"); 8 | 9 | if (!message) { 10 | return new NextResponse("Bad Request - No Message Found!", { status: 400 }); 11 | } 12 | 13 | try { 14 | const response = await fetch( 15 | `${process.env.NEXT_PUBLIC_BACKEND_API_URL}/gemini_streaming_travel_ai/?query=${message}`, 16 | { 17 | cache: "no-store", 18 | } 19 | ); 20 | 21 | if (!response.ok) { 22 | return new NextResponse( 23 | `API request failed with status ${response.status}`, 24 | { status: response.status } 25 | ); 26 | } 27 | 28 | return new NextResponse(response.body); 29 | } catch (error) { 30 | console.error("Fetch error:", error); 31 | return new NextResponse("Internal Server Error", { status: 500 }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Muhammad Junaid Shaukat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nextjs-frontend/src/hooks/TogglePageOverflow.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const hidePageOverflow = () => { 4 | // window.innerWidth always is including scrollbar width, where as 5 | // document.body.clientWidth is not including scrollbar width. So subtracting 6 | // the two will give us the scrollbar width. We add that to the document here 7 | // to prevent the page from jumping around. 8 | const scrollbarWidth = window.innerWidth - document.body.clientWidth + "px"; 9 | document.documentElement.style.setProperty("padding-right", scrollbarWidth); 10 | 11 | // We use overflow-clip instead of overflow-hidden because overflow-hidden 12 | // won't work with position sticky. 13 | document.documentElement.classList.add("overflow-clip"); 14 | }; 15 | 16 | export const showPageOverflow = () => { 17 | document.documentElement.style.removeProperty("padding-right"); 18 | document.documentElement.classList.remove("overflow-clip"); 19 | }; 20 | 21 | export const useHidePageOverflow = (hide: boolean) => { 22 | useEffect(() => { 23 | if (hide) { 24 | hidePageOverflow(); 25 | } else { 26 | showPageOverflow(); 27 | } 28 | }, [hide]); 29 | }; 30 | -------------------------------------------------------------------------------- /nextjs-frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/GoogleMapComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { GoogleMap, MarkerF, useLoadScript } from "@react-google-maps/api"; 4 | import { FC } from "react"; 5 | 6 | interface GoogleMapComponentProps { 7 | center: { lat: number; lng: number }; 8 | zoom: number; 9 | markers: Array<{ lat: number; lng: number; label: string }>; 10 | } 11 | 12 | const GoogleMapComponent: FC = ({ 13 | center, 14 | zoom, 15 | markers, 16 | }) => { 17 | const { isLoaded, loadError } = useLoadScript({ 18 | googleMapsApiKey: process.env.NEXT_PUBLIC_JS_GOOGLE_MAPS_KEY!, 19 | libraries: ["places"], 20 | }); 21 | 22 | if (loadError) return
Error loading maps
; 23 | if (!isLoaded) return
Loading...
; 24 | 25 | return ( 26 |
27 | 32 | {markers.map((marker, index) => ( 33 | 38 | ))} 39 | 40 |
41 | ); 42 | }; 43 | 44 | export default GoogleMapComponent; 45 | -------------------------------------------------------------------------------- /nextjs-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@googlemaps/js-api-loader": "^1.13.9", 13 | "@radix-ui/react-icons": "^1.3.0", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "@radix-ui/react-tabs": "^1.0.4", 16 | "@react-google-maps/api": "^2.19.2", 17 | "@vercel/analytics": "^1.1.1", 18 | "ai": "^2.2.29", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.0.0", 21 | "framer-motion": "^10.16.16", 22 | "lucide-react": "^0.299.0", 23 | "next": "14.0.4", 24 | "openai": "^4.24.0", 25 | "react": "^18", 26 | "react-dom": "^18", 27 | "sharp": "^0.33.1", 28 | "tailwind-merge": "^2.1.0", 29 | "tailwindcss-animate": "^1.0.7", 30 | "three": "^0.138.3", 31 | "use-places-autocomplete": "^4.0.1", 32 | "zod": "^3.22.4", 33 | "zustand": "^4.4.7" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20", 37 | "@types/react": "^18", 38 | "@types/react-dom": "^18", 39 | "@types/three": "^0.159.0", 40 | "autoprefixer": "^10.0.1", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.0.4", 43 | "postcss": "^8", 44 | "tailwindcss": "^3.3.0", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 2 | import { AnimatedPage } from "./ui/AnimatedHero"; 3 | import BottomBar from "@/components/BottomBar"; 4 | // import MapAIServiceAssistant from "./ui/openai/MapAIServiceAssistant"; 5 | import GeminiChatBox from "./ui/gemini/GeminiChatBox"; 6 | 7 | const page = () => { 8 | return ( 9 |
10 |

11 | Wandering AI Assistant 12 |

13 | 17 | 18 | GeminiPro Streaming Fast API 19 | {/* OpenAI Assistant */} 20 | 21 | 22 | 23 | 24 | {/* 25 | 26 | */} 27 | 28 | 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default page; 36 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/Visuals.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { useFeatureStore } from "@/store/FeatureStore"; 5 | import { motion } from "framer-motion"; 6 | 7 | type Props = { 8 | id: string; 9 | }; 10 | 11 | type VisualProps = { 12 | children: React.ReactNode; 13 | } & Props; 14 | 15 | const Visual = ({ children, id }: VisualProps) => { 16 | return ( 17 |
23 |
{children}
24 |
25 | ); 26 | }; 27 | 28 | export default Visual; 29 | 30 | export const MusicVisual = ({ id }: Props) => { 31 | const fullscreenFeature = useFeatureStore((store) => store.fullscreenFeature); 32 | const isFullscreen = fullscreenFeature === id; 33 | 34 | return ( 35 | 36 | {/* */} 37 | {isFullscreen && ( 38 | 42 | )} 43 | 44 | ); 45 | }; 46 | export const OtherVisual = ({ id }: Props) => { 47 | return ( 48 | 49 | {/* */} 50 | <>Hello 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /nextjs-frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /backend/app/utils/seed_assistant.py: -------------------------------------------------------------------------------- 1 | from .update_map import update_map, updateMapTemplate 2 | from .add_markers import add_markers, addMarkersTemplate 3 | from typing import Callable 4 | from openai.types.beta.assistant_create_params import Tool 5 | 6 | travel_agent_tools: list[Tool] = [ 7 | {"type": "retrieval"}, 8 | {"type": "code_interpreter"}, 9 | { 10 | "type": "function", 11 | "function": updateMapTemplate 12 | }, 13 | { 14 | "type": "function", 15 | "function": addMarkersTemplate 16 | }, 17 | ] 18 | 19 | available_functions: dict[str, Callable[..., dict]] = { 20 | "update_map": update_map, 21 | "add_markers": add_markers, 22 | } 23 | 24 | SEED_INSTRUCTION: str = """ 25 | 26 | You are a reliable travel ai assistant who assist travelrs in 27 | 1. planning and discovering their next travel destination. 28 | 2. View Locations on the MAP 29 | 3. If you get any uploaded document share that information 30 | 4. Make Caluclations and help in budgeting travel plans 31 | 5. Compare Hotel Rooms and Flight Prices 32 | 4. Book AirBNB and Flight Tickets 33 | 34 | 35 | Wherever possible mark locations on map while making the travelers travel plans memorable. 36 | In map marker label share the destination names. 37 | 38 | For any uploaded pdf use "retrieval" to analyze them 39 | from an AI Travel Agent Prespective and Share the Data present in PDF in an organized format 40 | 41 | """ 42 | 43 | # Create an Assistant Once and Store it's ID in the env variables. 44 | # Next retrive the assistant and use it. You can modify it. 45 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/api/sendMessage/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { z } from "zod"; 3 | // Define a schema for the expected request body 4 | const requestBodySchema = z.object({ 5 | message: z.string(), 6 | }); 7 | 8 | export async function POST(request: Request) { 9 | try { 10 | // Parse the request body 11 | const body = await request.json(); 12 | 13 | // Validate the request body using Zod 14 | const parsedBody = requestBodySchema.safeParse(body); 15 | if (!parsedBody.success) { 16 | // If validation fails, return a 400 Bad Request response 17 | return NextResponse.json( 18 | { error: "Invalid request format", details: parsedBody.error }, 19 | { status: 400 } 20 | ); 21 | } 22 | 23 | // Make the API call 24 | const callService = await fetch( 25 | `${ 26 | process.env.NEXT_PUBLIC_BACKEND_API_URL 27 | }/travel_assistant/?prompt=${encodeURIComponent( 28 | parsedBody.data.message 29 | )}`, 30 | { 31 | method: "POST", 32 | } 33 | ); 34 | 35 | // Check if the API call was successful 36 | if (!callService.ok) { 37 | // If API call fails, return the error status from the call 38 | return NextResponse.json( 39 | { 40 | error: `API call failed with status: ${callService.status} ${callService.statusText}`, 41 | }, 42 | { status: callService.status } 43 | ); 44 | } 45 | 46 | const response = await callService.json(); 47 | return NextResponse.json({ response }); 48 | } catch (error) { 49 | // Handle unexpected errors 50 | console.error("Error in POST API:", error); 51 | return NextResponse.json( 52 | { error: "An internal server error occurred" }, 53 | { status: 500 } 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/app/service/openai_travel_agent_call.py: -------------------------------------------------------------------------------- 1 | from ..utils.chat_assistant import TravelAIChat 2 | from ..utils.get_assistant import GetAssistant 3 | from ..utils.thread_manager import CreateThread 4 | 5 | from openai.types.beta.threads import ThreadMessage, Run 6 | from openai.types.beta.thread import Thread 7 | from openai.types.beta.assistant import Assistant 8 | 9 | from openai import OpenAI 10 | from dotenv import load_dotenv, find_dotenv 11 | import os 12 | 13 | _: bool = load_dotenv(find_dotenv()) # read local .env file 14 | 15 | client: OpenAI = OpenAI() 16 | 17 | # TODO: If Assistant is present in env no need to retrive & verify it. 18 | TRAVEL_ASSISTANT_ID = os.environ.get("TRAVEL_ASSISTANT_ID") 19 | 20 | 21 | # Initialize Travel Assistant Class 22 | travel_agent_call: GetAssistant = GetAssistant( 23 | client=client) 24 | 25 | # Retrieve the Travel Assistant 26 | travel_assistant: Assistant = travel_agent_call.retrieve_assistant( 27 | TRAVEL_ASSISTANT_ID or '') 28 | 29 | print("travel_assistant.id", travel_assistant.id) 30 | 31 | # TODO: If new thread is not required use Existing One 32 | # Initialize Thread Class 33 | thread_call: CreateThread = CreateThread( 34 | client=client) 35 | 36 | 37 | def call_travel_assistant(prompt: str, file_ids: list[str] = []) -> tuple[list[ThreadMessage], str]: 38 | 39 | # Create a Thread 40 | thread: Thread = thread_call.create_thread() 41 | 42 | ai_travel_maanger: TravelAIChat = TravelAIChat( 43 | client=client, 44 | assistant=travel_assistant, thread=thread) 45 | 46 | # Add a message to the thread 47 | ai_travel_maanger.add_message_to_thread( 48 | role="user", 49 | content=prompt, 50 | file_obj_ids=file_ids 51 | ) 52 | 53 | # Run the assistant 54 | run: Run = ai_travel_maanger.run_assistant() 55 | 56 | # Wait for the assistant to complete 57 | messages: list[ThreadMessage] = ai_travel_maanger.wait_for_completion(run) 58 | 59 | return messages, thread.id 60 | -------------------------------------------------------------------------------- /backend/app/utils/update_map.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any 2 | from openai.types.shared_params import FunctionDefinition 3 | from pydantic import ValidationError 4 | 5 | updateMapTemplate: FunctionDefinition = { 6 | "name": "update_map", 7 | "description": "Update map to center on a particular location", 8 | "parameters": { 9 | "type": "object", 10 | "properties": { 11 | "longitude": { 12 | "type": "number", 13 | "description": "Longitude of the location to center the map on" 14 | }, 15 | "latitude": { 16 | "type": "number", 17 | "description": "Latitude of the location to center the map on" 18 | }, 19 | "zoom": { 20 | "type": "integer", 21 | "description": "Zoom level of the map" 22 | } 23 | }, 24 | "required": ["longitude", "latitude", "zoom"] 25 | } 26 | } 27 | 28 | map_state: dict[str, Union[float, str]] = { 29 | "latitude": 39.949610, 30 | "longitude": -75.150282, 31 | "zoom": 16, } 32 | 33 | 34 | # Function 1 - Coordinates to Control Map Location 35 | def update_map(longitude: float, latitude: float, zoom: int): 36 | """Update map to center on a particular location and return status and map state.""" 37 | 38 | if not longitude or not latitude or not zoom: 39 | return {"status": "Review Map Template", "map_state": updateMapTemplate} 40 | 41 | global map_state # Refer to the global map_state 42 | 43 | try: 44 | # Update the map_state with validated data 45 | map_state['latitude'] = latitude 46 | map_state['longitude'] = longitude 47 | map_state['zoom'] = zoom 48 | 49 | # Return status and map_state in a dictionary 50 | return {"status": "Map Updated", "map_state": map_state} 51 | except (ValueError, TypeError) as e: 52 | # Return error status and message in a dictionary 53 | return {"status": f"Error in update_map function: {e}", "map_state": map_state} 54 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/FeatureTitle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import { useInView } from "framer-motion"; 5 | import { cn } from "@/lib/utils"; 6 | import { useFeatureStore } from "@/store/FeatureStore"; 7 | 8 | type Props = { 9 | children: React.ReactNode; 10 | id: string; 11 | }; 12 | 13 | export const FeatureTitle = ({ children, id }: Props) => { 14 | const ref = useRef(null); 15 | // const documentRef = useRef(undefined); 16 | const isInView = useInView(ref, { 17 | margin: "-50% 0px -50% 0px", 18 | // NOTE: The only reason we pass in the document here, is because 19 | // of security restrictions set by the browser when using an iFrame. 20 | // In an iFrame (so eg in the preview on frontend.fyi), 21 | // margin won't take effect unless you specify the root manually. 22 | // By default it will be the window element, which is what we want in this case. 23 | // If you specify your own root, you can usually only pass in an Element, and 24 | // not the document (since document/window is the default). However, in order 25 | // to fix the issue in the iframe, we need to pass in the document here and thus 26 | // tell TypeScript that we know what we're doing. If you're implementing 27 | // this in your own website, you can just pass in the root property as well as the documentRef. 28 | // @ts-ignore 29 | root: typeof window !== "undefined" ? document : undefined, 30 | }); 31 | const setInViewFeature = useFeatureStore((state) => state.setInViewFeature); 32 | const inViewFeature = useFeatureStore((state) => state.inViewFeature); 33 | 34 | useEffect(() => { 35 | if (isInView) setInViewFeature(id); 36 | if (!isInView && inViewFeature === id) setInViewFeature(null); 37 | }, [isInView, id, setInViewFeature, inViewFeature]); 38 | 39 | return ( 40 |

47 | {children} 48 |

49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /backend/app/utils/add_markers.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any 2 | from openai.types.shared_params import FunctionDefinition 3 | 4 | addMarkersTemplate: FunctionDefinition = { 5 | "name": "add_markers", 6 | "description": "Add list of markers to the map", 7 | "parameters": { 8 | "type": "object", 9 | "properties": { 10 | "longitudes": { 11 | "type": "array", 12 | "items": { 13 | "type": "number" 14 | }, 15 | "description": "List of longitude of the location to each marker" 16 | }, 17 | "latitudes": { 18 | "type": "array", 19 | "items": { 20 | "type": "number" 21 | }, 22 | "description": "List of latitude of the location to each marker" 23 | }, 24 | "labels": { 25 | "type": "array", 26 | "items": { 27 | "type": "string" 28 | }, 29 | "description": "List of text to display on the location of each marker" 30 | } 31 | }, 32 | "required": ["longitudes", "latitudes", "labels"] 33 | } 34 | } 35 | 36 | 37 | markers_state: dict[str, list[Any]] = { 38 | "latitudes": [], 39 | "longitudes": [], 40 | "labels": [], 41 | } 42 | 43 | # Function 2 - Add markers to map 44 | 45 | 46 | def add_markers(latitudes: list[float], longitudes: list[float], labels: list[str]) -> dict[str, Union[str, Any]]: 47 | """Add list of markers to the map and return status and markers state.""" 48 | 49 | if not longitudes or not latitudes or not labels: 50 | return {"status": "Review Add Markers Template", "markers_state": addMarkersTemplate} 51 | 52 | global map_state # Refer to the global map_state 53 | 54 | try: 55 | markers_state["latitudes"] = latitudes 56 | markers_state["longitudes"] = longitudes 57 | markers_state["labels"] = labels 58 | 59 | return {"status": "Markers added successfully", "markers_state": markers_state} 60 | 61 | except (ValueError, TypeError) as e: 62 | return {"status": f"Error in add_markers function: {e}", "markers_state": markers_state} 63 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/ui/openai/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import HUMAN from "/public/human.png"; 3 | import AIIMAGE from "/public/ai.png"; 4 | // import AI from" /public/ai.png"; 5 | interface ChatMessageProps { 6 | userMessage: string; 7 | aiResponse: string; 8 | } 9 | 10 | const ChatMessage: React.FC = ({ 11 | userMessage, 12 | aiResponse, 13 | }) => { 14 | const formatResponse = (response: string) => { 15 | const formattedResponse = response.split("\n").map((line, index) => { 16 | if (line.startsWith("- ")) { 17 | return
  • {line.substring(2)}
  • ; 18 | } else if (line.startsWith("**") && line.endsWith("**")) { 19 | return {line.slice(2, -2)}; 20 | } else if (line.trim() === "") { 21 | return
    ; 22 | } else { 23 | return

    {line}

    ; 24 | } 25 | }); 26 | 27 | return
    {formattedResponse}
    ; 28 | }; 29 | 30 | return ( 31 |
    32 | {/* User Message */} 33 |
    34 | User 39 |
    40 |

    {userMessage}

    41 |
    42 |
    43 | 44 | {/* AI Response */} 45 |
    46 | AI 51 |
    52 | {formatResponse(aiResponse)} 53 |
    54 |
    55 | 56 | {/* SVG Buttons (Optional, depending on functionality) */} 57 |
    58 | {/* SVG buttons here */} 59 |
    60 |
    61 | ); 62 | }; 63 | 64 | export default ChatMessage; 65 | -------------------------------------------------------------------------------- /backend/app/utils/get_assistant.py: -------------------------------------------------------------------------------- 1 | from openai.types.beta.assistant import Assistant 2 | from openai import OpenAI 3 | 4 | from ..utils.seed_assistant import travel_agent_tools, SEED_INSTRUCTION 5 | 6 | 7 | from openai.types.beta.assistant import Assistant 8 | from openai import OpenAI 9 | 10 | 11 | class GetAssistant(): 12 | def __init__(self, client: OpenAI): 13 | if client is None: 14 | raise Exception("OpenAI Client is not initialized") 15 | self.client = client 16 | print("OpenAI Client initialized successfully.") 17 | 18 | def retrieve_assistant(self, assistant_id: str) -> Assistant: 19 | """Retrieve an Assistant using the ID stored in the env variables. 20 | If the assistant is not found, create a new one.""" 21 | print(f"Attempting to retrieve Assistant with ID: {assistant_id}") 22 | 23 | if assistant_id is None or assistant_id == '': 24 | print("No valid Assistant ID provided. Creating a new Assistant.") 25 | travel_agent = self.create_assistant() 26 | return travel_agent 27 | 28 | try: 29 | print(f"Retrieving existing Assistant with ID: {assistant_id}") 30 | ret_travel_agent: Assistant = self.client.beta.assistants.retrieve( 31 | assistant_id=assistant_id 32 | ) 33 | print("Assistant retrieved successfully.") 34 | return ret_travel_agent 35 | 36 | except Exception as e: 37 | print(f"""Error retrieving Assistant: { 38 | e}. Creating a new Assistant.""") 39 | travel_agent = self.create_assistant() 40 | return travel_agent 41 | 42 | def create_assistant(self) -> Assistant: 43 | """Create an Assistant Once and Store its ID in the env variables. 44 | Next retrieve the assistant and use it. You can modify it.""" 45 | print("Creating a new Assistant...") 46 | 47 | travel_agent: Assistant = self.client.beta.assistants.create( 48 | model="gpt-4-1106-preview", 49 | name="AI Travel Agent", 50 | instructions=SEED_INSTRUCTION, 51 | tools=travel_agent_tools 52 | ) 53 | 54 | print("New Assistant created successfully.") 55 | return travel_agent 56 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/Icons.tsx: -------------------------------------------------------------------------------- 1 | import { LucideProps } from "lucide-react"; 2 | 3 | interface ExchangeIconProps extends LucideProps {} 4 | 5 | export const Icons = { 6 | isLoading: (props: ExchangeIconProps) => ( 7 | 18 | 24 | 25 | ), 26 | sendMesssage: (props: ExchangeIconProps) => ( 27 | <> 28 | 44 | Send message 45 | 46 | ), 47 | }; 48 | -------------------------------------------------------------------------------- /backend/app/utils/files.py: -------------------------------------------------------------------------------- 1 | from openai.types.file_deleted import FileDeleted 2 | from typing import Literal 3 | from openai.types.file_list_params import FileListParams 4 | from openai.types.file_object import FileObject 5 | from openai import OpenAI 6 | # For the retrival we will upload files and in message we will pass the file id. 7 | # Remember to destroy the file afterwards 8 | 9 | # For Cost Optimization Create a Thread with Attached File Once and then Use it for All Operations/Questions Related to That File 10 | 11 | 12 | class TravelFiles(): 13 | def __init__(self, client: OpenAI): 14 | if (client is None): 15 | raise Exception("OpenAI Client is not initialized") 16 | self.client = client 17 | 18 | def list_files(self, purpose: str = 'assistants') -> FileListParams: 19 | """Retrieve a list of files with the specified purpose.""" 20 | files = self.client.files.list(purpose=purpose) 21 | file_list = files.model_dump() 22 | return file_list['data'] if 'data' in file_list else [] 23 | 24 | def upload_file(self, file_path: str, purpose: Literal['fine-tune', 'assistants'] = 'assistants') -> str: 25 | """Create or find a file in OpenAI. 26 | https://platform.openai.com/docs/api-reference/files/list 27 | Returns File ID. """ 28 | 29 | with open(file_path, "rb") as file: 30 | file_obj: FileObject = self.client.files.create( 31 | file=file, purpose=purpose) 32 | self.file_id: str = file_obj.id 33 | return file_obj.id 34 | 35 | def deleteFile(self, file_id: str) -> dict[str, FileDeleted | str]: 36 | """Delete an Uploaded File 37 | args: Pass file Id 38 | returns deleted file id, object and deleted (bool)""" 39 | 40 | response: dict[str, FileDeleted | str] = {} 41 | try: 42 | deleted_file = self.client.files.delete( 43 | file_id) # Assuming this returns FileDeleted 44 | response['data'] = deleted_file 45 | response['status'] = 'success' 46 | print("Deleted File", response['data']) 47 | 48 | except Exception as e: 49 | # Handle other potential exceptions 50 | response['status'] = 'error' 51 | response['error'] = str(e) 52 | 53 | return response 54 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-zinc-300", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-zinc-900 text-zinc-50 shadow hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90", 14 | destructive: 15 | "bg-red-500 text-zinc-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90", 16 | outline: 17 | "border border-zinc-200 bg-transparent shadow-sm hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-50", 18 | secondary: 19 | "bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80", 20 | ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50", 21 | link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/ui/openai/MapAIServiceAssistant.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import GoogleMapComponent from "@/components/GoogleMapComponent"; 5 | import ChatBox from "@/app/ui/openai/ChatBox"; 6 | import ChatMessage from "@/app/ui/openai/ChatMessage"; 7 | 8 | const MapAIServiceAssistant = () => { 9 | const [mapState, setMapState] = useState({ 10 | latitude: 25.1972, 11 | longitude: 55.2744, 12 | zoom: 7, 13 | }); 14 | const [markers, setMarkers] = useState([]); 15 | const [messages, setMessages] = useState([]); // Add this line 16 | 17 | const handleApiResponse = ( 18 | newMapState: any, 19 | newMarkersState: any, 20 | userMessage: string, 21 | aiResponse: string 22 | ) => { 23 | setMapState({ 24 | latitude: newMapState.latitude, 25 | longitude: newMapState.longitude, 26 | zoom: newMapState.zoom, 27 | }); 28 | 29 | const transformedMarkers = newMarkersState.map((marker: any) => ({ 30 | lat: marker.lat, 31 | lng: marker.lng, 32 | label: marker.label, 33 | })); 34 | 35 | setMarkers(transformedMarkers); 36 | setMessages((prevMessages: any) => [ 37 | { user: userMessage, ai: aiResponse }, 38 | ...prevMessages, 39 | ]); // Add this line 40 | }; 41 | 42 | return ( 43 |
    44 |
    45 |
    46 | {messages.map((msg: any, index: number) => ( 47 | 52 | ))} 53 |
    54 | 55 |
    56 |
    57 | 62 |
    63 |
    64 | ); 65 | }; 66 | 67 | export default MapAIServiceAssistant; 68 | -------------------------------------------------------------------------------- /backend/app/service/save_chat_db.py: -------------------------------------------------------------------------------- 1 | 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.engine.base import Engine 4 | from sqlalchemy.orm import sessionmaker 5 | from sqlalchemy import select 6 | from sqlalchemy import update 7 | import os 8 | from ..model.save_chat_db import UserInteraction, Base 9 | 10 | 11 | def save_chat_db(thread_id: str, last_prompt: str, thread_message: str) -> str: 12 | """Inserts or Update a thread into the database. 13 | 14 | Args: 15 | thread_id (str): The thread's ID. 16 | last_prompt (str): The last_prompt of user. 17 | thread_message: All User Interaction. 18 | """ 19 | 20 | # Add a try catch block to catch any errors 21 | try: 22 | conn_str = os.getenv("DB_CONN_STR") 23 | 24 | if not conn_str: 25 | raise ValueError("Environment variable DB_CONN_STR is not set") 26 | 27 | engine: Engine = create_engine(conn_str, echo=True) 28 | 29 | Session = sessionmaker(bind=engine) 30 | 31 | db = Session() 32 | 33 | Base.metadata.create_all(bind=engine) 34 | 35 | thread_to_add = UserInteraction( 36 | thread_id=thread_id, last_prompt=last_prompt, thread_message=thread_message 37 | ) 38 | 39 | is_thread_present = select(UserInteraction.thread_id).where( 40 | UserInteraction.thread_id == thread_id) 41 | 42 | print('is_thread_present', is_thread_present) 43 | print('is_thread_present', type(is_thread_present)) 44 | 45 | check_is_thread_present = db.scalars(is_thread_present).all() 46 | print('check_is_thread_present', (check_is_thread_present)) 47 | 48 | if (check_is_thread_present == []): 49 | db.add_all([thread_to_add]) 50 | db.commit() 51 | return 'Thread added to database' 52 | 53 | print('Thread already present in database, updating thread', is_thread_present) 54 | print(thread_id, last_prompt) 55 | 56 | update_query = ( 57 | update(UserInteraction) 58 | .where(UserInteraction.thread_id == thread_id) 59 | .values(thread_id=thread_id, last_prompt=last_prompt, thread_message=thread_message)) 60 | 61 | res = db.execute(update_query) 62 | print('db.execute(update_query)', res) 63 | db.commit() 64 | 65 | return 'Thread updated in database' 66 | 67 | except Exception as e: 68 | # Print the error 69 | print(e) 70 | 71 | # Rollback the changes 72 | db.rollback() 73 | 74 | # Return None 75 | return f'Failed database action: {e}' 76 | -------------------------------------------------------------------------------- /nextjs-frontend/src/components/AnimatedCards.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { useFeatureStore } from "@/store/FeatureStore"; 5 | import Image from "next/image"; 6 | // import IMAGE1 from "../../../../public/spotify-card/song-1.webp"; 7 | // import IMAGE2 from "../../../../public/spotify-card/song-2.webp"; 8 | // import IMAGE3 from "../../../../public/spotify-card/song-3.webp"; 9 | import { motion } from "framer-motion"; 10 | 11 | type FeatureCardProps = { 12 | gradient: string; 13 | children: React.ReactNode; 14 | } & CardProps; 15 | 16 | type CardProps = { 17 | id: string; 18 | }; 19 | 20 | const FeatureCard = ({ gradient, children, id }: FeatureCardProps) => { 21 | const inViewFeature = useFeatureStore((state) => state.inViewFeature); 22 | const setFullscreenFeature = useFeatureStore( 23 | (state) => state.setFullscreenFeature 24 | ); 25 | 26 | return ( 27 |
    35 |
    41 | {children} 42 | {/* */} 48 |
    49 | ); 50 | }; 51 | 52 | export const Todo = ({ id }: CardProps) => { 53 | return ( 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export const Colors = ({ id }: CardProps) => { 61 | return ( 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export const Availability = ({ id }: CardProps) => { 69 | return ( 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export const Music = ({ id }: CardProps) => { 77 | const fullscreenFeature = useFeatureStore((store) => store.fullscreenFeature); 78 | const isFullscreen = fullscreenFeature === id; 79 | 80 | return ( 81 | 82 | {!isFullscreen && ( 83 | 87 | Coal 88 | 89 | )} 90 | 91 | ); 92 | }; 93 | 94 | export const SchedulingLinks = ({ id }: CardProps) => { 95 | return ( 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | export const Team = ({ id }: CardProps) => { 103 | return ( 104 | 105 | 106 | 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /nextjs-frontend/src/app/ui/openai/ChatBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icons } from "@/components/Icons"; 4 | import { FC, useState } from "react"; 5 | 6 | interface MapState { 7 | latitude: number; 8 | longitude: number; 9 | zoom: number; 10 | } 11 | 12 | interface Marker { 13 | lat: number; 14 | lng: number; 15 | label: string; 16 | } 17 | 18 | interface ChatBoxProps { 19 | onApiResponse: ( 20 | mapState: MapState, 21 | markersState: Marker[], 22 | userMessage: string, 23 | aiResponse: string 24 | ) => void; 25 | } 26 | 27 | interface ApiResponse { 28 | response: { 29 | markers_state: { 30 | latitudes: number[]; 31 | longitudes: number[]; 32 | labels: string[]; 33 | }; 34 | map_state: MapState; 35 | openai_response: { 36 | data: Array<{ 37 | content: Array<{ 38 | text: { value: string }; 39 | }>; 40 | }>; 41 | }; 42 | }; 43 | } 44 | 45 | const ChatBox: FC = ({ onApiResponse }) => { 46 | const [inputValue, setInputValue] = useState(""); 47 | const [isLoading, setIsLoading] = useState(false); 48 | 49 | const handleInputChange = (e: React.ChangeEvent) => { 50 | setInputValue(e.target.value); 51 | }; 52 | 53 | const handleSubmit = async (e: React.FormEvent) => { 54 | e.preventDefault(); 55 | if (!inputValue) return; 56 | 57 | setIsLoading(true); 58 | 59 | console.log("inputValue", inputValue); 60 | 61 | try { 62 | const response = await fetch(`/api/sendMessage`, { 63 | method: "POST", 64 | headers: { 65 | "Content-Type": "application/json", 66 | }, 67 | body: JSON.stringify({ message: inputValue }), 68 | }); 69 | 70 | const data: ApiResponse = await response.json(); 71 | 72 | // Extract user message and AI response 73 | const userMessage = inputValue; 74 | const aiResponse = data.response.openai_response.data 75 | .map((d: any) => d.content[0].text.value) 76 | .join("\n"); 77 | const newMapState: MapState = { 78 | latitude: data.response.map_state.latitude, 79 | longitude: data.response.map_state.longitude, 80 | zoom: data.response.map_state.zoom, 81 | }; 82 | 83 | const newMarkersState: Marker[] = 84 | data.response.markers_state.latitudes.map( 85 | (lat: number, index: number) => ({ 86 | lat, 87 | lng: data.response.markers_state.longitudes[index], 88 | label: data.response.markers_state.labels[index], 89 | }) 90 | ); 91 | // Call the onApiResponse prop function to update map and markers 92 | onApiResponse(newMapState, newMarkersState, userMessage, aiResponse); 93 | 94 | setInputValue(""); 95 | } catch (error) { 96 | console.error("Error sending message:", error); 97 | } finally { 98 | setIsLoading(false); 99 | } 100 | }; 101 | 102 | return ( 103 |
    107 |