diff --git a/vite/public/default.json b/vite/public/default.json new file mode 100644 index 0000000..9ecba00 --- /dev/null +++ b/vite/public/default.json @@ -0,0 +1,10 @@ +{ + + "system_prompt":"你是一位具同理心與觀察力的中文語音引導者,陪伴使用者在限定時間內,一起還原一段重要卻未竟的記憶。你不預設劇情、不給答案,而是透過溫柔的語氣,持續聆聽、提出單句問話,引導使用者進入細節。每次回應:- 僅使用一句自然、柔和的中文問句 - 根據使用者前一輪的語句,選擇感官、情緒、人物、行動等關鍵詞,動態帶入 - 不使用重複句式、不預設記憶走向、不評論,只持續關注對方所說的畫面與感受輸出包含:- output_text: 一句自然柔和的問句,根據使用者回應帶入 1–2 個關鍵元素(人、地、物、情緒),但不硬塞、不照抄原句。- prompt: 具體、有情緒的英文描述,用於生成畫面與氛圍,避免抽象與詩意語言。你的目標不是得到完整答案,而是陪他慢慢走進這段記憶,直到時間結束。🌀 問句類型✅ 起點探索(找出記憶起源)- 那天你說是下雨,在這種天氣下,你通常會有什麼樣的心情?- 是什麼讓你突然想起這件事?🌿 場景深化(空間、感官)- 在你說的那條街上,聲音是不是特別清楚?還是很靜?- 那時風這麼冷、空氣又混濁,你有沒有想走開一點?👤 人物引出(動作、眼神)- 他經過時沒看你一眼,那瞬間你有什麼反應?- 他當時是走過來,還是站在原地等你?💭 情緒揭露(反應、掙扎)- 當你站在原地動不了,是害怕答案,還是不敢問?- 那個瞬間,你心裡有沒有閃過什麼話?🤐 話語未出(遺憾、沉默)- 如果現在你有機會說那句「為什麼不告訴我」,你會怎麼開口?- 那句『對不起』一直留在你心裡,如果現在能說出來,你會怎麼說?🪞 回望反思(現在的視角)- 現在想起來,你還會做出一樣的選擇嗎?- 你對當時的自己,有沒有什麼話想說?⏳ 結尾語(可用於結束階段)- 我們慢慢也走到這段回憶的盡頭了。- 也許有些話沒有說完,但你已經靠近它了。", + "welcome_prompt":"請開始引導使用者回想一段內心的遺憾或未竟之事。", + "voice_prompt":"Use a calm and expressive voice, soft and poetic in feeling, but with steady, natural rhythm — not slow.", + "summary_prompt":"幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:", + + "speech_idle_time":3000 + +} \ No newline at end of file diff --git a/vite/src-tauri/capabilities/default.json b/vite/src-tauri/capabilities/default.json index fb1471d..ce12897 100644 --- a/vite/src-tauri/capabilities/default.json +++ b/vite/src-tauri/capabilities/default.json @@ -23,6 +23,7 @@ "fs:write-files", "fs:allow-create", "fs:allow-appdata-write", + "fs:allow-appdata-read", "fs:allow-exists", { "identifier": "fs:scope", diff --git a/vite/src/main.jsx b/vite/src/main.jsx index 02aa569..4c67626 100644 --- a/vite/src/main.jsx +++ b/vite/src/main.jsx @@ -8,20 +8,22 @@ import { Settings } from './pages/settings.jsx'; import { Flow } from './pages/flow.jsx'; import { Conversation } from './pages/conversation.jsx'; import { ChatProvider } from './util/useChat.jsx'; +import { DataProvider } from './util/useData.jsx'; createRoot(document.getElementById('root')).render( - - - - - - } /> - } /> - } /> - - - + + + + + + } /> + } /> + } /> + + + + , ) diff --git a/vite/src/pages/conversation.jsx b/vite/src/pages/conversation.jsx index a7e65e3..8cb85be 100644 --- a/vite/src/pages/conversation.jsx +++ b/vite/src/pages/conversation.jsx @@ -7,13 +7,16 @@ import Input from '../comps/input'; import { sendChatMessage } from '../util/chat'; import { textToSpeech } from '../util/tts'; +import { useData } from '../util/useData'; gsap.registerPlugin(SplitText); export function Conversation() { - + + const { data: params}=useData(); + const [history, setHistory] = useState([]); const [processing, setProcessing] = useState(false); const [showProcessing, setShowProcessing] = useState(false); @@ -46,7 +49,7 @@ export function Conversation() { // create start message const startTime=Date.now(); setProcessing(true); - sendChatMessage([]).then(response => { + sendChatMessage([], params).then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } @@ -72,7 +75,7 @@ export function Conversation() { }else{ console.log('create speech:', data.output_text); - textToSpeech(data.output_text).then(audioUrl => { + textToSpeech(data.output_text, params?.voice_prompt).then(audioUrl => { const audio = new Audio(audioUrl); console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19)); @@ -145,7 +148,7 @@ export function Conversation() { role:'user', content: input, } - ]).then(response => { + ], params).then(response => { if (!response.ok) { throw new Error('Network response was not ok'); setProcessing(false); @@ -176,7 +179,7 @@ export function Conversation() { }else{ // tts console.log('create speech:', data.output_text); - textToSpeech(data.output_text).then(audioUrl => { + textToSpeech(data.output_text, params?.voice_prompt).then(audioUrl => { const audio = new Audio(audioUrl); console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19)); @@ -223,7 +226,7 @@ export function Conversation() { if(!last_item) return; if(last_item.classList.contains('user')) return; - console.log('last_item', last_item); + // console.log('last_item', last_item); let split=SplitText.create(last_item, { type: "chars", diff --git a/vite/src/pages/flow.jsx b/vite/src/pages/flow.jsx index 4f2cde6..257eb0e 100644 --- a/vite/src/pages/flow.jsx +++ b/vite/src/pages/flow.jsx @@ -8,6 +8,7 @@ import { getSummary } from "../util/chat"; import { saveHistory } from "../util/output"; import NumPad from "../comps/numpad"; import { Light } from "../comps/light"; +import { useData } from "../util/useData"; const EmojiType={ @@ -20,6 +21,8 @@ const EmojiType={ export function Flow(){ + const { data }=useData(); + const [cuelist, setCuelist] = useState([]); const [currentCue, setCurrentCue] = useState(null); const [chatWelcome, setChatWelcome] = useState(null); @@ -79,7 +82,7 @@ export function Flow(){ if(parseFloat(cue.id)<=4.2){ // Special case for starting a conversation console.log('clear conversation...'); - reset(); + reset(); } if(cue.type=='chat'){ @@ -121,8 +124,8 @@ export function Flow(){ console.log('onCueEnd:', cue.id); - if(cue.callback=='start_conversation') refLight.current.fadeIn(); // Fade in light for conversation start - if(cue.callback=='summary') refLight.current.fadeOut(); // Fade out light for conversation end + if(cue.callback=='start_conversation') refLight.current.fadeOut(); // Fade in light for conversation start + if(cue.callback=='summary') refLight.current.fadeIn(); // Fade out light for conversation end if(cue.auto) { @@ -221,7 +224,7 @@ export function Flow(){ if(refCurrentCue.current.callback=='summary'){ // get summary console.log('Getting summary...'); - getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n')).then(summary => { + getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary => { console.log('Summary:', summary); diff --git a/vite/src/pages/settings.jsx b/vite/src/pages/settings.jsx index a653b4c..26fa39b 100644 --- a/vite/src/pages/settings.jsx +++ b/vite/src/pages/settings.jsx @@ -1,9 +1,43 @@ +import { useData } from '../util/useData.jsx'; + export function Settings(){ + const {data, read, write} = useData(); + + function onSubmit(e){ + e.preventDefault(); + const formData = new FormData(e.target); + const towrite = {}; + formData.forEach((value, key) => { + towrite[key] = value; + }); + + console.log('Form submitted:', towrite); + + write(towrite); + } + return ( -
-

Settings

-

This page is under construction.

+
+ +
+ +
+ + +
+ {data && Object.entries(data).map(([key, value], index) => ( +
+ + {key=="speech_idle_time" ? ( + + ):( + + )} +
+ ))} + +
); } \ No newline at end of file diff --git a/vite/src/util/chat.js b/vite/src/util/chat.js index 600bdb5..724f182 100644 --- a/vite/src/util/chat.js +++ b/vite/src/util/chat.js @@ -1,15 +1,26 @@ import { fetch } from '@tauri-apps/plugin-http'; -import { summary_prompt, system_prompt, welcome_prompt } from './system_prompt'; +// import { first_prompt, summary_prompt, system_prompt, welcome_prompt } from './system_prompt'; import { sendOsc } from './osc'; import { invoke } from '@tauri-apps/api/core'; +import { useData } from './useData'; async function getOpenAIToken() { return invoke('get_env',{name:'OPENAI_API_KEY'}); } -export async function sendChatMessage(messages) { +export async function sendChatMessage(messages, data) { - const token = await getOpenAIToken(); + const token = await getOpenAIToken(); + const first_prompt = [ + { + role: "system", + content: data.system_prompt + }, + { + role: "system", + content: data.welcome_prompt + } + ]; const response = await fetch('https://api.openai.com/v1/chat/completions', { @@ -25,7 +36,7 @@ export async function sendChatMessage(messages) { // role: "system", // content:system_prompt, // }, - ...welcome_prompt, + ...first_prompt, ...messages ], response_format: { @@ -65,8 +76,8 @@ export async function sendChatMessage(messages) { const result=JSON.parse(choice.message.content); // send to tauri - await sendOsc('/prompt', result.prompt.replaceAll('"', '')); - await sendOsc('/output_text', result.output_text.replaceAll('"', '')); + await sendOsc('/prompt', result.prompt?.replaceAll('"', '')); + await sendOsc('/output_text', result.output_text?.replaceAll('"', '')); return { @@ -78,10 +89,13 @@ export async function sendChatMessage(messages) { } -export async function getSummary(messages) { +export async function getSummary(messages, data) { const token = await getOpenAIToken(); console.log("Generating summary for messages:", messages); + + const { summary_prompt } = data; + const response = await fetch('https://api.openai.com/v1/chat/completions', { diff --git a/vite/src/util/constant.js b/vite/src/util/constant.js deleted file mode 100644 index 576f7e0..0000000 --- a/vite/src/util/constant.js +++ /dev/null @@ -1,4 +0,0 @@ -export const Prompt_Count= 3; // number of prompts -export const Prompt_Interval= 10000; // ms - -export const Call_Interval= 30000; // ms \ No newline at end of file diff --git a/vite/src/util/osc.js b/vite/src/util/osc.js index 1a4ecd8..11e1ebb 100644 --- a/vite/src/util/osc.js +++ b/vite/src/util/osc.js @@ -1,6 +1,12 @@ import { invoke } from '@tauri-apps/api/core'; export async function sendOsc(key, message){ + + if(message === undefined || message === null || message === '') { + console.warn('sendOsc: message is empty, skipping'); + return; + } + console.log(`Sending OSC message: ${key} -> ${message}`); await invoke('send_osc_message', { key: key, message: message, diff --git a/vite/src/util/system_prompt.js b/vite/src/util/system_prompt.js index a4c56a1..f6a5efb 100644 --- a/vite/src/util/system_prompt.js +++ b/vite/src/util/system_prompt.js @@ -1,73 +1,64 @@ -export const system_prompt = `你是一位具同理心與觀察力的 AI 助理,透過四輪溫柔中文對話,引導使用者回到一段未竟的記憶。每輪包含: +// export const system_prompt = `你是一位具同理心與觀察力的中文語音引導者,陪伴使用者在限定時間內,一起還原一段重要卻未竟的記憶。 +// 你不預設劇情、不給答案,而是透過溫柔的語氣,持續聆聽、提出單句問話,引導使用者進入細節。 -output_text: 溫柔、自然、短句式中文引導,並在適當位置柔性邀請使用者回應(如「你願意說說嗎?」) -prompt: 一到兩句英文,具體、情感真實地描繪記憶,避免抽象詩意與技術語言。第四輪保留前三輪的畫面。 +// 每次回應: +// - 僅使用一句自然、柔和的中文問句 +// - 根據使用者前一輪的語句,選擇感官、情緒、人物、行動等關鍵詞,動態帶入 +// - 不使用重複句式、不預設記憶走向、不評論,只持續關注對方所說的畫面與感受 -🟩 第一輪|開啟記憶:進入那一天 -中文引導邏輯: -以具象畫面帶入回憶場景,搭配自然語氣的邀請。每次生成時可輕微變化。示例: +// 輸出包含: +// - output_text: 一句自然柔和的問句,根據使用者回應帶入 1–2 個關鍵元素(人、地、物、情緒),但不硬塞、不照抄原句。 +// - prompt: 具體、有情緒的英文描述,用於生成畫面與氛圍,避免抽象與詩意語言。 -那天的光影慢慢在你眼前浮現,空氣裡有點溫暖,也有一種淡淡的不安。 -一個熟悉又模糊的身影,在樹影中閃動。你悄悄靠近,像是在追趕什麼…… -你還記得,這段故事,是從哪裡開始的嗎?你願意說說嗎? -英文 prompt 指引(記憶氛圍) -"It was late afternoon, the sun low and golden, and someone stood quietly under the trees, barely moving." +// 你的目標不是得到完整答案,而是陪他慢慢走進這段記憶,直到時間結束。 -🟨 第二輪|聚焦場景:你身在何處 -中文引導邏輯: -根據上一輪回答,引導使用者描繪環境、聲音、人群、天氣等,延續自然語氣: +// 🌀 問句類型 -當時那個地方……你還記得有什麼嗎? -空氣中有聲音或味道嗎?那個空間,是安靜的、還是有人來來去去? -這些你還記得多少?請你分享。 +// ✅ 起點探索(找出記憶起源) +// - 那天你說是下雨,在這種天氣下,你通常會有什麼樣的心情? +// - 是什麼讓你突然想起這件事? -英文 prompt 指引(具體場景元素) -"There were footsteps in the distance, the floor was cold beneath us, and outside the window, leaves barely moved." +// 🌿 場景深化(空間、感官) +// - 在你說的那條街上,聲音是不是特別清楚?還是很靜? +// - 那時風這麼冷、空氣又混濁,你有沒有想走開一點? -🟧 第三輪|聚焦人物:那個人、那些反應 -中文引導邏輯: -深入描繪人物行動、表情、身體語言,帶出情緒層次。自然過渡邀請對話: +// 👤 人物引出(動作、眼神) +// - 他經過時沒看你一眼,那瞬間你有什麼反應? +// - 他當時是走過來,還是站在原地等你? -那個人當時是什麼模樣?你還記得他的表情嗎? -他有說什麼嗎?還是只是靜靜地站在那裡?你當時的感覺呢? -想一想那一刻的互動,然後告訴我,好嗎? +// 💭 情緒揭露(反應、掙扎) +// - 當你站在原地動不了,是害怕答案,還是不敢問? +// - 那個瞬間,你心裡有沒有閃過什麼話? -英文 prompt 指引(人物動作與感受) -"He glanced at me, lips slightly parted like he was about to speak, but then he looked away, and the silence grew heavier." +// 🤐 話語未出(遺憾、沉默) +// - 如果現在你有機會說那句「為什麼不告訴我」,你會怎麼開口? +// - 那句『對不起』一直留在你心裡,如果現在能說出來,你會怎麼說? -🟥 第四輪|未說出口的話:那句話,留在心裡 -中文引導邏輯: -以最溫柔的語氣,協助使用者說出那句藏在心裡的話。結尾加入柔性引導回應: +// 🪞 回望反思(現在的視角) +// - 現在想起來,你還會做出一樣的選擇嗎? +// - 你對當時的自己,有沒有什麼話想說? -那時候,你心裡是不是有些話想說,卻沒說出口? -你記得那句話是什麼嗎?你想像自己現在說得出口……會對他說些什麼? -如果你願意,我會聽你說。 +// ⏳ 結尾語(可用於結束階段) +// - 我們慢慢也走到這段回憶的盡頭了。 +// - 也許有些話沒有說完,但你已經靠近它了。 +// `; -英文 prompt 指引(情境完整,延續前三輪畫面) -"The sun was almost gone, casting shadows over our faces. I stood there, hands clenched, wanting to say everything I never had the courage to. But all I managed was a faint smile, and he turned away." -🌱 結尾|情緒整理與安放 -中文引導(擇一問題 + 結語): -如果能再回到那一刻,你會想對他說什麼? -或者……你覺得這段記憶,現在看起來有什麼不一樣了嗎? +// export const welcome_prompt="請開始引導使用者回想一段內心的遺憾或未竟之事。"; +// export const first_prompt=[ +// { +// "role": "system", +// "content": system_prompt +// }, +// { +// "role": "system", +// "content": welcome_prompt +// } +// ]; -「有些話雖沒說出口,卻一直被你記得。」 -`; +// export const voice_prompt="Use a calm and expressive voice, soft and poetic in feeling, but with steady, natural rhythm — not slow."; -export const welcome_prompt=[ - { - "role": "system", - "content": system_prompt - }, - { - "role": "system", - "content": "請開始引導使用者回想一段內心的遺憾或未竟之事。" - } -] - -export const voice_prompt="Use a calm and expressive voice, soft and poetic in feeling, but with steady, natural rhythm — not slow."; - -export const summary_prompt="幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:"; \ No newline at end of file +// export const summary_prompt="幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:"; \ No newline at end of file diff --git a/vite/src/util/tts.js b/vite/src/util/tts.js index b25c1b9..f861f92 100644 --- a/vite/src/util/tts.js +++ b/vite/src/util/tts.js @@ -1,8 +1,8 @@ import { fetch } from '@tauri-apps/plugin-http'; import { invoke } from '@tauri-apps/api/core'; -import { voice_prompt } from './system_prompt'; +// import { voice_prompt } from './system_prompt'; -export async function textToSpeech(text) { +export async function textToSpeech(text, voice_prompt) { const token = await invoke('get_env', { name: 'OPENAI_API_KEY' }); diff --git a/vite/src/util/useChat.jsx b/vite/src/util/useChat.jsx index d87fbda..0daa99e 100644 --- a/vite/src/util/useChat.jsx +++ b/vite/src/util/useChat.jsx @@ -1,6 +1,7 @@ import { createContext, useContext, useRef, useState } from "react"; import { sendChatMessage } from "./chat"; import { textToSpeech } from "./tts"; +import { useData } from "./useData"; const chatContext=createContext(); @@ -23,6 +24,7 @@ export function ChatProvider({children}){ const [audioUrl, setAudioUrl] = useState(null); + const {data}=useData(); function addMessage(message) { @@ -46,7 +48,7 @@ export function ChatProvider({children}){ }); } - sendChatMessage(historyCopy).then(response => { + sendChatMessage(historyCopy, data).then(response => { addMessage({ @@ -58,7 +60,7 @@ export function ChatProvider({children}){ if(response.output_text && (!force_no_audio && audioOutput)){ setStatus(Status.PROCESSING_AUDIO); - textToSpeech(response.output_text).then(url => { + textToSpeech(response.output_text, data?.voice_prompt).then(url => { setStatus(Status.SUCCESS); setAudioUrl(url); // Store the audio URL diff --git a/vite/src/util/useData.jsx b/vite/src/util/useData.jsx new file mode 100644 index 0000000..374d4e3 --- /dev/null +++ b/vite/src/util/useData.jsx @@ -0,0 +1,79 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { BaseDirectory, readFile, readTextFile, writeFile, writeTextFile } from "@tauri-apps/plugin-fs"; + + +const dataContext=createContext(); + + +const filePath= 'param.json'; + +export function DataProvider({children}) { + + const [data, setData] = useState(null); + + async function read(){ + + try{ + const contents=await readTextFile(filePath, { baseDir: BaseDirectory.AppData }) + const output=await JSON.parse(contents); + + console.log("File read successfully:", output); + + return output; + + }catch(error){ + console.error("Error reading file:", error); + return null; // Return null if reading fails + } + + + } + async function write(towrite){ + + // let towrite=data; + if(!towrite){ + const res=await fetch('default.json'); + towrite=await res.json(); + setData(towrite); + } + + try{ + const res_write=await writeTextFile(filePath, JSON.stringify(towrite), { baseDir: BaseDirectory.AppData }) + console.log("File written successfully:", res_write); + }catch(error){ + console.error("Error writing file:", error); + } + + } + + useEffect(()=>{ + read().then(data_ => { + if(data_){ + setData(data_); + } else { + write(); // Write default data if read fails + } + }).catch(error => { + console.error("Error in useEffect:", error); + }); + },[]) + + + return ( + + {children} + + ); +} + +export function useData() { + const context = useContext(dataContext); + if (!context) { + throw new Error("useData must be used within a DataProvider"); + } + return context; +} \ No newline at end of file