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.
+
);
}
\ 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