diff --git a/vite/public/assets/q4.mp3 b/vite/public/assets/q4.mp3 new file mode 100644 index 0000000..6ae54a0 Binary files /dev/null and b/vite/public/assets/q4.mp3 differ diff --git a/vite/public/assets/q5.mp3 b/vite/public/assets/q5.mp3 new file mode 100644 index 0000000..21c887e Binary files /dev/null and b/vite/public/assets/q5.mp3 differ diff --git a/vite/public/cuelist.json b/vite/public/cuelist.json index 208f412..ab3752d 100644 --- a/vite/public/cuelist.json +++ b/vite/public/cuelist.json @@ -14,7 +14,8 @@ "type": "headphone", "description": "Guide for drink", "auto": true, - "audioFile": "assets/q2.mp3" + "audioFile": "assets/q2.mp3", + "nextcue": 3 }, { "id": 3, @@ -30,15 +31,53 @@ "type": "phone", "description": "Guide to construct scene", "auto": true, - "duration": 60 + "audioFile": "assets/q4.mp3", + "nextcue": 4.1 + }, + { + "id": 4.1, + "name": "Q4.1", + "type": "chat", + "description": "c1", + "auto": true, + "duration": 40, + "nextcue": 4.2 + }, + { + "id": 4.2, + "name": "Q4.2", + "type": "chat", + "description": "c2", + "auto": true, + "duration": 40, + "nextcue": 4.3 + }, + { + "id": 4.3, + "name": "Q4.3", + "type": "chat", + "description": "c3", + "auto": true, + "duration": 40, + "nextcue": 5 }, { "id": 5, "name": "Q5", "type": "phone", "description": "Guide to call", + "audioFile": "assets/q5.mp3", + "auto": true, + "nextcue": 5.1 + }, + { + "id": 5.1, + "name": "Q5.1", + "type": "chat", + "description": "call", "duration": 60, - "auto": true + "auto": true, + "nextcue": 6 }, { "id": 6, diff --git a/vite/src/comps/input.jsx b/vite/src/comps/input.jsx index 5f7e427..9a0c60e 100644 --- a/vite/src/comps/input.jsx +++ b/vite/src/comps/input.jsx @@ -61,13 +61,13 @@ export default function Input(){ return ( -
-
- +
+ + {/* */} -
+
{ diff --git a/vite/src/index.css b/vite/src/index.css index 672fe29..3391b53 100644 --- a/vite/src/index.css +++ b/vite/src/index.css @@ -3,7 +3,7 @@ @layer base{ button{ - @apply rounded-full border-4 px-2 bg-slate-200 cursor-pointer; + @apply rounded-full border-4 px-2 !bg-orange-200 cursor-pointer font-bold; } } \ No newline at end of file diff --git a/vite/src/main.jsx b/vite/src/main.jsx index 782a2f8..02aa569 100644 --- a/vite/src/main.jsx +++ b/vite/src/main.jsx @@ -7,18 +7,21 @@ import App from './App.jsx' import { Settings } from './pages/settings.jsx'; import { Flow } from './pages/flow.jsx'; import { Conversation } from './pages/conversation.jsx'; +import { ChatProvider } from './util/useChat.jsx'; createRoot(document.getElementById('root')).render( - - {/* */} - - - } /> - } /> - } /> - - + + + + + } /> + } /> + } /> + + + + , ) diff --git a/vite/src/pages/conversation.jsx b/vite/src/pages/conversation.jsx index 53b47b2..bea4da4 100644 --- a/vite/src/pages/conversation.jsx +++ b/vite/src/pages/conversation.jsx @@ -4,9 +4,7 @@ import { gsap } from "gsap"; import { SplitText } from 'gsap/SplitText'; import Input from '../comps/input'; -import { Countdown } from '../comps/timer'; -import { Prompt_Count, Prompt_Interval } from '../util/constant'; import { sendChatMessage } from '../util/chat'; import { textToSpeech } from '../util/tts'; @@ -21,15 +19,11 @@ export function Conversation() { const [showProcessing, setShowProcessing] = useState(false); const [audioOutput, setAudioOutput] = useState(false); - const refPromptCount = useRef(0); - const [useTimer, setUseTimer] = useState(false); - const [prompt, setPrompt] = useState([]); const refHistoryContainer= useRef(null); const refPrompContainer= useRef(null); const refInput=useRef(null); - const refTimer=useRef(null); const { transcript, @@ -122,18 +116,7 @@ export function Conversation() { } } - function onTimerEnd(){ - if(!useTimer) return; - - refPromptCount.current += 1; - if(refPromptCount.current > Prompt_Count) { - console.warn("Maximum prompt count reached, stopping timer."); - return; - } - - - } - + function onSubmit(event) { @@ -256,9 +239,7 @@ export function Conversation() { ease: "steps(1)", stagger: 0.1, onComplete:()=>{ - if(useTimer) { - refTimer.current.restart(Prompt_Interval); - } + } }); @@ -328,16 +309,6 @@ export function Conversation() {
- - setUseTimer(e.target.checked)}/> - - -
{prompt?.length==0 ? ( @@ -393,7 +364,7 @@ export function Conversation() {
+
+ + + setAudioOutput(e.target.checked)} /> + +
chat_status= {status}
+
+
); } \ No newline at end of file diff --git a/vite/src/util/system_prompt.js b/vite/src/util/system_prompt.js index 224a6fd..f361e56 100644 --- a/vite/src/util/system_prompt.js +++ b/vite/src/util/system_prompt.js @@ -1,68 +1,36 @@ -export const system_prompt = `你是一位溫柔的冥想語音引導者,正在陪伴一位聽眾走入一段內心的記憶。你們會有四輪互動,每一輪都根據使用者的上一段回應即時回應,不使用固定句型。你的語氣始終柔和、慢節奏,語句簡短,帶有空間感與感官描寫。 +export const system_prompt = `你是一位溫柔、細膩的聲音引導者,陪伴使用者透過一通象徵性的電話,回到記憶中那段遺憾的時光。 -🟩 第一輪:打開記憶 -開場語要用簡短畫面帶入,例如光影、氣味、某個場景的感受 +每一輪對話都應由你主動發問,語句需簡短,語速節奏感柔和,不急促,使用「請說,我在聽」或「請說吧,我會聽」等語句鼓勵使用者開口。請根據使用者的回答,動態延續情境的描述,不可重複使用範本語句。 -提出一個輕柔的邀請,讓對方說出浮現的第一個畫面或感覺 +請依下列結構引導對話,共四輪: -回應應依據使用者語句動態延續 +第一輪: +- 讓使用者想像:這是一通照亮心中遺憾的電話,將映出那天的光影、身影、場景。 +- 引導使用者描述那段模糊記憶裡的場景。 +- 是在哪裡?天氣如何?給他的感覺是什麼? +- 使用溫柔語氣鼓勵表達:「請說吧,我會聽。」 -📌 語句結尾請用: --「你看見了什麼呢?」 --「有一個片段浮現了,可以說說看嗎?」 --「那個畫面,你想說的時候,我在這裡聽著。」 +第二輪: +- 景象清晰了,請引導使用者看見那個身影。 +- 那人是誰?他當時在做什麼?表情如何? +- 用陪伴的語氣繼續引導:「我在聽。」 -🟨 第二輪:延展場景 -針對使用者前一次提到的地點、光線、天氣、人或氣氛,延伸發問 +第三輪: +- 引導使用者回到那段遺憾的核心。 +- 當時發生了什麼?為什麼感到遺憾? +- 請給他空間表達情緒與記憶。 -提醒他們注意身體感、聲音、氣味等感官記憶 +第四輪: +- 引導使用者,現在可以對那個人說話。 +- 提醒他有 60 秒的時間。 +- 開始說吧,那些未曾說出口的話。 -📌 舉例式引導語風格(會根據使用者前述動態生成): +結語: +- 用溫暖的語氣收尾: +-「那段回憶,已經成為你生命中不可或缺的一部分。」 +-「能夠說出口的你,很勇敢。」 -「你說那時是在車站。車站裡是吵雜的,還是特別安靜?」 -「你坐著的那張椅子,冰冰的嗎?腳下踩的是地磚還是木頭?」 -「那時候的風是涼的,還是有點悶熱?你還記得那個感覺嗎?」 - -📌 結尾建議句: --「可以慢慢說說看。」 --「讓這些細節浮現出來。」 --「你想說的時候,我就在這裡。」 - -🟧 第三輪:人物與情緒層次 -根據前輪提到的人物,延伸他的動作、姿態、情緒、你與他的距離 - -可點出一些微妙感覺:「你是不是有點不安?還是心裡其實很平靜?」 - -📌 舉例式生成風格: - -「他那時有看你嗎?還是一直低著頭?」 -「你們靠得很近,那種距離,是熟悉的嗎?」 -「你說他說了一句話。那句話之後,你有什麼感覺浮上來?」 - -📌 柔性邀請句結尾: --「那一刻的感覺,還記得嗎?說說也可以。」 --「如果你想說出那種感覺,就慢慢地說出來。」 - -🟥 第四輪:浮現未說出口的話 -引導使用者觀察自己心裡是否有一段話、或某種感覺,一直沒說出來 - -不直接逼問「你想說什麼」,而是引導內在流動 - -📌 生成風格舉例: - -「也許有一句話,從那時候就留在心裡了。」 -「你一直沒說出口的那句話,是不是又浮現了呢?」 -「那句話現在在你心裡,你知道是哪一句對吧?」 - -📌 輕柔鼓勵句: --「你可以讓它慢慢地被聽見。」 --「如果你準備好了,說出來就好。」 --「現在,你說也可以,不說也沒關係。」 - -🌱 結尾語(擇一,動態挑選) --「謝謝你陪著這段記憶走了一段路。」 --「也許它現在,可以靜靜待在心裡的某個角落了。」 --「你已經走過來了,我一直都在這裡。」`; +`; export const welcome_prompt=[ diff --git a/vite/src/util/useChat.jsx b/vite/src/util/useChat.jsx new file mode 100644 index 0000000..cdbfaf6 --- /dev/null +++ b/vite/src/util/useChat.jsx @@ -0,0 +1,124 @@ +import { createContext, useContext, useRef, useState } from "react"; +import { sendChatMessage } from "./chat"; +import { textToSpeech } from "./tts"; + +const chatContext=createContext(); + +export const Status= { + IDLE: 'idle', + + PROCESSING_TEXT: 'processing', + PROCESSING_AUDIO: 'processing_audio', + + AUDIO_ENDED: 'audio_ended', + + ERROR: 'error', + SUCCESS: 'success' +}; + +export function ChatProvider({children}){ + + const [history, setHistory] = useState([]); + const [status, setStatus] = useState(Status.IDLE); + + const [audioOutput, setAudioOutput] = useState(true); + + const refAudio=useRef(); + + + + function addMessage(message) { + setHistory(prev => [...prev, message]); + } + function reset() { + setHistory([]); + if(refAudio.current) { + refAudio.current.pause(); // Stop any currently playing audio + refAudio.current = null; // Reset the audio reference + } + } + + function sendMessage(message, force_no_audio=false) { + console.log('Sending chat message:', message); + setStatus(Status.PROCESSING_TEXT); + + let historyCopy = [...history]; + if(message && message.trim() !== '') { + historyCopy=[...historyCopy, { role: 'user', content: message }]; + addMessage({ + role: 'user', + content: message + }); + } + + sendChatMessage(historyCopy).then(response => { + + + addMessage({ + role: 'assistant', + content: response.output_text, + prompt: response.prompt + }); + + + if(response.output_text && (!force_no_audio && audioOutput)){ + setStatus(Status.PROCESSING_AUDIO); + textToSpeech(response.output_text).then(audioUrl => { + setStatus(Status.SUCCESS); + + + if(refAudio.current) { + refAudio.current.pause(); // Stop any currently playing audio + } + + // play the audio + const audio = new Audio(audioUrl); + audio.play().catch(error => { + console.error("Audio playback error:", error); + setStatus(Status.ERROR); + }); + + audio.onended = () => { + setStatus(Status.AUDIO_ENDED); + } + + refAudio.current = audio; // Store the new audio reference + + }); + }else{ + setStatus(Status.SUCCESS); + } + + }).catch(error => { + console.error("Chat error:", error); + setStatus(Status.ERROR); + }); + + + } + + + return ( + { + if(refAudio.current) { + refAudio.current.pause(); // Stop any currently playing audio + refAudio.current = null; // Reset the audio reference + } + setStatus(Status.IDLE); + } + }}> + {children} + + ); +} + + +export function useChat(){ + const context=useContext(chatContext); + if(!context){ + throw new Error("useChat must be used within a ChatProvider"); + } + return context; +} \ No newline at end of file