mirror of
https://gitee.com/blackfox/geekai.git
synced 2025-12-06 16:58:24 +08:00
更改工作流组件
This commit is contained in:
parent
a3a2500498
commit
0746cd49f4
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -38,7 +38,8 @@
|
|||||||
"v3-waterfall": "^1.3.3",
|
"v3-waterfall": "^1.3.3",
|
||||||
"vant": "^4.5.0",
|
"vant": "^4.5.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-router": "^4.0.15"
|
"vue-router": "^4.0.15",
|
||||||
|
"vue-waterfall-plugin-next": "^2.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.6",
|
"@babel/core": "7.18.6",
|
||||||
@ -12951,6 +12952,11 @@
|
|||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-waterfall-plugin-next": {
|
||||||
|
"version": "2.6.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-waterfall-plugin-next/-/vue-waterfall-plugin-next-2.6.5.tgz",
|
||||||
|
"integrity": "sha512-8ACGbdjoyKLiJfnKXB8h8f9eE14lhyzfI1N1nrfVAIRczSpNY1KRwGOnVXN5OHqheLl3V1C0uVVRPtjTJkHkhw=="
|
||||||
|
},
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@openai/realtime-api-beta": "github:openai/openai-realtime-api-beta",
|
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
@ -33,12 +32,18 @@
|
|||||||
"pinia": "^2.1.4",
|
"pinia": "^2.1.4",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"qs": "^6.11.1",
|
"qs": "^6.11.1",
|
||||||
|
"@better-scroll/core": "^2.5.1",
|
||||||
|
"@better-scroll/mouse-wheel": "^2.5.1",
|
||||||
|
"@better-scroll/observe-dom": "^2.5.1",
|
||||||
|
"@better-scroll/pull-up": "^2.5.1",
|
||||||
|
"@better-scroll/scroll-bar": "^2.5.1",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"three": "^0.128.0",
|
"three": "^0.128.0",
|
||||||
"v3-waterfall": "^1.3.3",
|
"v3-waterfall": "^1.3.3",
|
||||||
"vant": "^4.5.0",
|
"vant": "^4.5.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-router": "^4.0.15"
|
"vue-router": "^4.0.15",
|
||||||
|
"vue-waterfall-plugin-next": "^2.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.6",
|
"@babel/core": "7.18.6",
|
||||||
|
|||||||
@ -330,12 +330,7 @@ const routes = [
|
|||||||
meta: { title: "测试页面" },
|
meta: { title: "测试页面" },
|
||||||
component: () => import("@/views/Test.vue"),
|
component: () => import("@/views/Test.vue"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "test2",
|
|
||||||
path: "/test2",
|
|
||||||
meta: { title: "测试页面" },
|
|
||||||
component: () => import("@/views/RealtimeTest.vue"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "NotFound",
|
name: "NotFound",
|
||||||
path: "/:all(.*)",
|
path: "/:all(.*)",
|
||||||
|
|||||||
@ -97,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
|
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
|
||||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
<div class="task-list-inner">
|
||||||
<div class="job-list-box">
|
<div class="job-list-box">
|
||||||
<h2 class="text-xl">任务列表</h2>
|
<h2 class="text-xl">任务列表</h2>
|
||||||
<task-list :list="runningJobs" />
|
<task-list :list="runningJobs" />
|
||||||
@ -105,7 +105,7 @@
|
|||||||
<h2 class="text-xl">创作记录</h2>
|
<h2 class="text-xl">创作记录</h2>
|
||||||
<div class="finish-job-list">
|
<div class="finish-job-list">
|
||||||
<div v-if="finishedJobs.length > 0">
|
<div v-if="finishedJobs.length > 0">
|
||||||
<v3-waterfall
|
<!-- <v3-waterfall
|
||||||
id="waterfall"
|
id="waterfall"
|
||||||
:list="finishedJobs"
|
:list="finishedJobs"
|
||||||
srcKey="img_thumb"
|
srcKey="img_thumb"
|
||||||
@ -196,9 +196,58 @@
|
|||||||
<i class="iconfont icon-face"></i>
|
<i class="iconfont icon-face"></i>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v3-waterfall>
|
</v3-waterfall> -->
|
||||||
|
<Waterfall
|
||||||
|
ref="waterfall"
|
||||||
|
:list="finishedJobs"
|
||||||
|
:row-key="options.rowKey"
|
||||||
|
:gutter="options.gutter"
|
||||||
|
:has-around-gutter="options.hasAroundGutter"
|
||||||
|
:width="options.width"
|
||||||
|
:breakpoints="options.breakpoints"
|
||||||
|
:img-selector="options.imgSelector"
|
||||||
|
:background-color="options.backgroundColor"
|
||||||
|
:animation-effect="options.animationEffect"
|
||||||
|
:animation-duration="options.animationDuration"
|
||||||
|
:animation-delay="options.animationDelay"
|
||||||
|
:animation-cancel="options.animationCancel"
|
||||||
|
:lazyload="options.lazyload"
|
||||||
|
:load-props="options.loadProps"
|
||||||
|
:cross-origin="options.crossOrigin"
|
||||||
|
:align="options.align"
|
||||||
|
@afterRender="afterRender"
|
||||||
|
>
|
||||||
|
<template #default="{ item, url, index }">
|
||||||
|
<div class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-lg hover:shadow-gray-600 group" @click="handleClick(item)">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<LazyImg :url="url" title="title" :alt="item.name" class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105" @load="imageLoad" @error="imageError" @success="imageSuccess" />
|
||||||
|
</div>
|
||||||
|
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
|
||||||
|
<h2 class="pb-4 text-gray-50 group-hover:text-yellow-300">
|
||||||
|
{{ item.name }}
|
||||||
|
</h2>
|
||||||
|
<div class="pt-3 flex justify-between items-center border-t border-t-gray-600 border-opacity-50">
|
||||||
|
<div class="text-gray-50">
|
||||||
|
$ {{ item.price }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="px-3 h-7 rounded-full bg-red-500 text-sm text-white shadow-lg transition-all duration-300 hover:bg-red-600" @click.stop="handleDelete(item, index)">
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Waterfall>
|
||||||
</div>
|
</div>
|
||||||
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else />
|
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else />
|
||||||
|
|
||||||
|
<div v-show="!loading" class="flex justify-center py-10 bg-gray-900">
|
||||||
|
<button class="px-5 py-2 rounded-full bg-gray-700 text-md text-white cursor-pointer hover:bg-gray-800 transition-all duration-300" @click="fetchFinishJobs">
|
||||||
|
加载更多
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -236,6 +285,11 @@ import { useSharedStore } from "@/store/sharedata";
|
|||||||
import TaskList from "@/components/TaskList.vue";
|
import TaskList from "@/components/TaskList.vue";
|
||||||
import BackTop from "@/components/BackTop.vue";
|
import BackTop from "@/components/BackTop.vue";
|
||||||
import { showMessageError, showMessageOK } from "@/utils/dialog";
|
import { showMessageError, showMessageOK } from "@/utils/dialog";
|
||||||
|
import BScrollBox from "@/components/ui/BScrollBox.vue";
|
||||||
|
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||||
|
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||||
|
|
||||||
|
import error from '@/assets/img/failed.png'
|
||||||
|
|
||||||
const listBoxHeight = ref(0);
|
const listBoxHeight = ref(0);
|
||||||
// const paramBoxHeight = ref(0)
|
// const paramBoxHeight = ref(0)
|
||||||
@ -251,6 +305,89 @@ const resizeElement = function () {
|
|||||||
listBoxHeight.value = window.innerHeight - 58;
|
listBoxHeight.value = window.innerHeight - 58;
|
||||||
// paramBoxHeight.value = window.innerHeight - 110
|
// paramBoxHeight.value = window.innerHeight - 110
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const options = ref({
|
||||||
|
// 唯一key值
|
||||||
|
rowKey: 'id',
|
||||||
|
// 卡片之间的间隙
|
||||||
|
gutter: 10,
|
||||||
|
// 是否有周围的gutter
|
||||||
|
hasAroundGutter: true,
|
||||||
|
// 卡片在PC上的宽度
|
||||||
|
width: 200,
|
||||||
|
// 自定义行显示个数,主要用于对移动端的适配
|
||||||
|
breakpoints: {
|
||||||
|
3840: {
|
||||||
|
// 4K下
|
||||||
|
rowPerView: 8,
|
||||||
|
},
|
||||||
|
2560: {
|
||||||
|
// 2K下
|
||||||
|
rowPerView: 7,
|
||||||
|
},
|
||||||
|
1920: {
|
||||||
|
// 2K下
|
||||||
|
rowPerView: 6,
|
||||||
|
},
|
||||||
|
1600: {
|
||||||
|
// 2K下
|
||||||
|
rowPerView: 5,
|
||||||
|
},
|
||||||
|
1366: {
|
||||||
|
// 2K下
|
||||||
|
rowPerView: 4,
|
||||||
|
},
|
||||||
|
800: {
|
||||||
|
// 当屏幕宽度小于等于800
|
||||||
|
rowPerView: 3,
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
// 当屏幕宽度小于等于500
|
||||||
|
rowPerView: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 动画效果
|
||||||
|
animationEffect: 'animate__fadeInUp',
|
||||||
|
// 动画时间
|
||||||
|
animationDuration: 1000,
|
||||||
|
// 动画延迟
|
||||||
|
animationDelay: 300,
|
||||||
|
animationCancel: false,
|
||||||
|
// 背景色
|
||||||
|
backgroundColor: '#2C2E3A',
|
||||||
|
// imgSelector
|
||||||
|
imgSelector: 'img_thumb',
|
||||||
|
// 加载配置
|
||||||
|
loadProps: {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
ratioCalculator: (width, height) => {
|
||||||
|
console.log("width, height", width, height)
|
||||||
|
return height / width
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 是否懒加载
|
||||||
|
lazyload: true,
|
||||||
|
align: 'center',
|
||||||
|
})
|
||||||
|
|
||||||
|
function imageLoad(url) {
|
||||||
|
console.log(`${url}: 加载完成`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageError(url) {
|
||||||
|
console.error(`${url}: 加载失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageSuccess(url) {
|
||||||
|
console.log(`${url}: 加载成功`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterRender() {
|
||||||
|
loading.value = false
|
||||||
|
console.log('计算完成')
|
||||||
|
}
|
||||||
|
|
||||||
resizeElement();
|
resizeElement();
|
||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
resizeElement();
|
resizeElement();
|
||||||
|
|||||||
@ -1,471 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div data-component="ConsolePage">
|
|
||||||
<div class="content-top">
|
|
||||||
<div class="content-title">
|
|
||||||
<img src="/openai-logomark.svg" alt="OpenAI Logo" />
|
|
||||||
<span>realtime console</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="content-main">
|
|
||||||
<div class="content-logs">
|
|
||||||
<div class="content-block events">
|
|
||||||
<div class="visualization">
|
|
||||||
<div class="visualization-entry client">
|
|
||||||
<canvas ref="clientCanvasRef" />
|
|
||||||
</div>
|
|
||||||
<div class="visualization-entry server">
|
|
||||||
<canvas ref="serverCanvasRef" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content-block-title">events</div>
|
|
||||||
<div class="content-block-body" ref="eventsScrollRef">
|
|
||||||
<template v-if="!realtimeEvents.length">
|
|
||||||
awaiting connection...
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div v-for="(realtimeEvent, i) in realtimeEvents" :key="realtimeEvent.event.event_id" class="event">
|
|
||||||
<div class="event-timestamp">
|
|
||||||
{{ formatTime(realtimeEvent.time) }}
|
|
||||||
</div>
|
|
||||||
<div class="event-details">
|
|
||||||
<div
|
|
||||||
class="event-summary"
|
|
||||||
@click="toggleEventDetails(realtimeEvent.event.event_id)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'event-source',
|
|
||||||
realtimeEvent.event.type === 'error'
|
|
||||||
? 'error'
|
|
||||||
: realtimeEvent.source,
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<component :is="realtimeEvent.source === 'client' ? ArrowUp : ArrowDown" />
|
|
||||||
<span>
|
|
||||||
{{ realtimeEvent.event.type === 'error'
|
|
||||||
? 'error!'
|
|
||||||
: realtimeEvent.source }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="event-type">
|
|
||||||
{{ realtimeEvent.event.type }}
|
|
||||||
{{ realtimeEvent.count ? `(${realtimeEvent.count})` : '' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="expandedEvents[realtimeEvent.event.event_id]"
|
|
||||||
class="event-payload"
|
|
||||||
>
|
|
||||||
{{ JSON.stringify(realtimeEvent.event, null, 2) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content-block conversation">
|
|
||||||
<div class="content-block-title">conversation</div>
|
|
||||||
<div class="content-block-body" data-conversation-content>
|
|
||||||
<template v-if="!items.length">
|
|
||||||
awaiting connection...
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div
|
|
||||||
v-for="(conversationItem, i) in items"
|
|
||||||
:key="conversationItem.id"
|
|
||||||
class="conversation-item"
|
|
||||||
>
|
|
||||||
<div :class="['speaker', conversationItem.role || '']">
|
|
||||||
<div>
|
|
||||||
{{
|
|
||||||
(conversationItem.role || conversationItem.type).replaceAll(
|
|
||||||
'_',
|
|
||||||
' '
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="close" @click="deleteConversationItem(conversationItem.id)">
|
|
||||||
<X />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="speaker-content">
|
|
||||||
<!-- tool response -->
|
|
||||||
<div v-if="conversationItem.type === 'function_call_output'">
|
|
||||||
{{ conversationItem.formatted.output }}
|
|
||||||
</div>
|
|
||||||
<!-- tool call -->
|
|
||||||
<div v-if="conversationItem.formatted.tool">
|
|
||||||
{{ conversationItem.formatted.tool.name }}(
|
|
||||||
{{ conversationItem.formatted.tool.arguments }})
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
!conversationItem.formatted.tool &&
|
|
||||||
conversationItem.role === 'user'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
conversationItem.formatted.transcript ||
|
|
||||||
(conversationItem.formatted.audio?.length
|
|
||||||
? '(awaiting transcript)'
|
|
||||||
: conversationItem.formatted.text || '(item sent)')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
!conversationItem.formatted.tool &&
|
|
||||||
conversationItem.role === 'assistant'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
conversationItem.formatted.transcript ||
|
|
||||||
conversationItem.formatted.text ||
|
|
||||||
'(truncated)'
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<audio
|
|
||||||
v-if="conversationItem.formatted.file"
|
|
||||||
:src="conversationItem.formatted.file.url"
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content-actions" style="position:absolute; top: 0; left: 0">
|
|
||||||
<el-button
|
|
||||||
:type="isConnected ? '' : 'primary'"
|
|
||||||
@click="connectConversation"
|
|
||||||
>
|
|
||||||
{{isConnected ? '断开连接' : '连接对话'}}
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<el-button @mousedown="startRecording" @mouseup="stopRecording">开始讲话</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
|
|
||||||
import { RealtimeClient } from '@openai/realtime-api-beta';
|
|
||||||
import { WavRecorder, WavStreamPlayer } from '@/lib/wavtools/index.js';
|
|
||||||
import { instructions } from '@/utils/conversation_config.js';
|
|
||||||
import { WavRenderer } from '@/utils/wav_renderer';
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const LOCAL_RELAY_SERVER_URL = process.env.REACT_APP_LOCAL_RELAY_SERVER_URL || '';
|
|
||||||
|
|
||||||
// Reactive state
|
|
||||||
const apiKey = ref(
|
|
||||||
LOCAL_RELAY_SERVER_URL
|
|
||||||
? ''
|
|
||||||
: localStorage.getItem('tmp::voice_api_key') || prompt('OpenAI API Key') || ''
|
|
||||||
);
|
|
||||||
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }));
|
|
||||||
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }));
|
|
||||||
const client = ref(
|
|
||||||
new RealtimeClient({
|
|
||||||
url: "ws://localhost:5678/api/realtime",
|
|
||||||
apiKey: "sk-Gc5cEzDzGQLIqxWA9d62089350F3454bB359C4A3Fa21B3E4",
|
|
||||||
dangerouslyAllowAPIKeyInBrowser: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const clientCanvasRef = ref(null);
|
|
||||||
const serverCanvasRef = ref(null);
|
|
||||||
const eventsScrollRef = ref(null);
|
|
||||||
const startTime = ref(new Date().toISOString());
|
|
||||||
|
|
||||||
const items = ref([]);
|
|
||||||
const realtimeEvents = ref([]);
|
|
||||||
const expandedEvents = reactive({});
|
|
||||||
const isConnected = ref(false);
|
|
||||||
const canPushToTalk = ref(true);
|
|
||||||
const isRecording = ref(false);
|
|
||||||
const memoryKv = ref({});
|
|
||||||
const coords = ref({ lat: 37.775593, lng: -122.418137 });
|
|
||||||
const marker = ref(null);
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const formatTime = (timestamp) => {
|
|
||||||
const t0 = new Date(startTime.value).valueOf();
|
|
||||||
const t1 = new Date(timestamp).valueOf();
|
|
||||||
const delta = t1 - t0;
|
|
||||||
const hs = Math.floor(delta / 10) % 100;
|
|
||||||
const s = Math.floor(delta / 1000) % 60;
|
|
||||||
const m = Math.floor(delta / 60_000) % 60;
|
|
||||||
const pad = (n) => {
|
|
||||||
let s = n + '';
|
|
||||||
while (s.length < 2) {
|
|
||||||
s = '0' + s;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
return `${pad(m)}:${pad(s)}.${pad(hs)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectConversation = async () => {
|
|
||||||
alert(123)
|
|
||||||
startTime.value = new Date().toISOString();
|
|
||||||
isConnected.value = true;
|
|
||||||
realtimeEvents.value = [];
|
|
||||||
items.value = client.value.conversation.getItems();
|
|
||||||
|
|
||||||
await wavRecorder.value.begin();
|
|
||||||
await wavStreamPlayer.value.connect();
|
|
||||||
await client.value.connect();
|
|
||||||
client.value.sendUserMessageContent([
|
|
||||||
{
|
|
||||||
type: 'input_text',
|
|
||||||
text: '你好,我是老阳!',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (client.value.getTurnDetectionType() === 'server_vad') {
|
|
||||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disconnectConversation = async () => {
|
|
||||||
isConnected.value = false;
|
|
||||||
realtimeEvents.value = [];
|
|
||||||
items.value = [];
|
|
||||||
memoryKv.value = {};
|
|
||||||
coords.value = { lat: 37.775593, lng: -122.418137 };
|
|
||||||
marker.value = null;
|
|
||||||
|
|
||||||
client.value.disconnect();
|
|
||||||
await wavRecorder.value.end();
|
|
||||||
await wavStreamPlayer.value.interrupt();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteConversationItem = async (id) => {
|
|
||||||
client.value.deleteItem(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startRecording = async () => {
|
|
||||||
isRecording.value = true;
|
|
||||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
|
|
||||||
if (trackSampleOffset?.trackId) {
|
|
||||||
const { trackId, offset } = trackSampleOffset;
|
|
||||||
await client.value.cancelResponse(trackId, offset);
|
|
||||||
}
|
|
||||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopRecording = async () => {
|
|
||||||
isRecording.value = false;
|
|
||||||
await wavRecorder.value.pause();
|
|
||||||
client.value.createResponse();
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeTurnEndType = async (value) => {
|
|
||||||
if (value === 'none' && wavRecorder.value.getStatus() === 'recording') {
|
|
||||||
await wavRecorder.value.pause();
|
|
||||||
}
|
|
||||||
client.value.updateSession({
|
|
||||||
turn_detection: value === 'none' ? null : { type: 'server_vad' },
|
|
||||||
});
|
|
||||||
if (value === 'server_vad' && client.value.isConnected()) {
|
|
||||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
|
|
||||||
}
|
|
||||||
canPushToTalk.value = value === 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEventDetails = (eventId) => {
|
|
||||||
if (expandedEvents[eventId]) {
|
|
||||||
delete expandedEvents[eventId];
|
|
||||||
} else {
|
|
||||||
expandedEvents[eventId] = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle hooks and watchers
|
|
||||||
onMounted(() => {
|
|
||||||
if (apiKey.value !== '') {
|
|
||||||
localStorage.setItem('tmp::voice_api_key', apiKey.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up render loops for the visualization canvas
|
|
||||||
let isLoaded = true;
|
|
||||||
const render = () => {
|
|
||||||
if (isLoaded) {
|
|
||||||
if (clientCanvasRef.value) {
|
|
||||||
const canvas = clientCanvasRef.value;
|
|
||||||
if (!canvas.width || !canvas.height) {
|
|
||||||
canvas.width = canvas.offsetWidth;
|
|
||||||
canvas.height = canvas.offsetHeight;
|
|
||||||
}
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
const result = wavRecorder.value.recording
|
|
||||||
? wavRecorder.value.getFrequencies('voice')
|
|
||||||
: { values: new Float32Array([0]) };
|
|
||||||
WavRenderer.drawBars(canvas, ctx, result.values, '#0099ff', 10, 0, 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (serverCanvasRef.value) {
|
|
||||||
const canvas = serverCanvasRef.value;
|
|
||||||
if (!canvas.width || !canvas.height) {
|
|
||||||
canvas.width = canvas.offsetWidth;
|
|
||||||
canvas.height = canvas.offsetHeight;
|
|
||||||
}
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
const result = wavStreamPlayer.value.analyser
|
|
||||||
? wavStreamPlayer.value.getFrequencies('voice')
|
|
||||||
: { values: new Float32Array([0]) };
|
|
||||||
WavRenderer.drawBars(canvas, ctx, result.values, '#009900', 10, 0, 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestAnimationFrame(render);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
render();
|
|
||||||
|
|
||||||
// Set up client event listeners
|
|
||||||
client.value.on('realtime.event', (realtimeEvent) => {
|
|
||||||
realtimeEvents.value = realtimeEvents.value.slice();
|
|
||||||
const lastEvent = realtimeEvents.value[realtimeEvents.value.length - 1];
|
|
||||||
if (lastEvent?.event.type === realtimeEvent.event.type) {
|
|
||||||
lastEvent.count = (lastEvent.count || 0) + 1;
|
|
||||||
realtimeEvents.value.splice(-1, 1, lastEvent);
|
|
||||||
} else {
|
|
||||||
realtimeEvents.value.push(realtimeEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.value.on('error', (event) => console.error(event));
|
|
||||||
|
|
||||||
client.value.on('conversation.interrupted', async () => {
|
|
||||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
|
|
||||||
if (trackSampleOffset?.trackId) {
|
|
||||||
const { trackId, offset } = trackSampleOffset;
|
|
||||||
await client.value.cancelResponse(trackId, offset);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.value.on('conversation.updated', async ({ item, delta }) => {
|
|
||||||
items.value = client.value.conversation.getItems();
|
|
||||||
if (delta?.audio) {
|
|
||||||
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id);
|
|
||||||
}
|
|
||||||
if (item.status === 'completed' && item.formatted.audio?.length) {
|
|
||||||
const wavFile = await WavRecorder.decode(
|
|
||||||
item.formatted.audio,
|
|
||||||
24000,
|
|
||||||
24000
|
|
||||||
);
|
|
||||||
item.formatted.file = wavFile;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up client instructions and tools
|
|
||||||
client.value.updateSession({ instructions: instructions });
|
|
||||||
client.value.updateSession({ input_audio_transcription: { model: 'whisper-1' } });
|
|
||||||
|
|
||||||
client.value.addTool(
|
|
||||||
{
|
|
||||||
name: 'set_memory',
|
|
||||||
description: 'Saves important data about the user into memory.',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
key: {
|
|
||||||
type: 'string',
|
|
||||||
description:
|
|
||||||
'The key of the memory value. Always use lowercase and underscores, no other characters.',
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Value can be anything represented as a string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['key', 'value'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ key, value }) => {
|
|
||||||
memoryKv.value = { ...memoryKv.value, [key]: value };
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
client.value.addTool(
|
|
||||||
{
|
|
||||||
name: 'get_weather',
|
|
||||||
description:
|
|
||||||
'Retrieves the weather for a given lat, lng coordinate pair. Specify a label for the location.',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
lat: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Latitude',
|
|
||||||
},
|
|
||||||
lng: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Longitude',
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Name of the location',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['lat', 'lng', 'location'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ lat, lng, location }) => {
|
|
||||||
marker.value = { lat, lng, location };
|
|
||||||
coords.value = { lat, lng, location };
|
|
||||||
const result = await fetch(
|
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,wind_speed_10m`
|
|
||||||
);
|
|
||||||
const json = await result.json();
|
|
||||||
const temperature = {
|
|
||||||
value: json.current.temperature_2m,
|
|
||||||
units: json.current_units.temperature_2m,
|
|
||||||
};
|
|
||||||
const wind_speed = {
|
|
||||||
value: json.current.wind_speed_10m,
|
|
||||||
units: json.current_units.wind_speed_10m,
|
|
||||||
};
|
|
||||||
marker.value = { lat, lng, location, temperature, wind_speed };
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
items.value = client.value.conversation.getItems();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
client.value.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watchers
|
|
||||||
watch(realtimeEvents, () => {
|
|
||||||
if (eventsScrollRef.value) {
|
|
||||||
const eventsEl = eventsScrollRef.value;
|
|
||||||
eventsEl.scrollTop = eventsEl.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(items, () => {
|
|
||||||
const conversationEls = document.querySelectorAll('[data-conversation-content]');
|
|
||||||
conversationEls.forEach((el) => {
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* You can add your component-specific styles here */
|
|
||||||
/* If you're using SCSS, you might want to import your existing SCSS file */
|
|
||||||
/* @import './ConsolePage.scss'; */
|
|
||||||
</style>
|
|
||||||
@ -24,6 +24,9 @@ module.exports = defineConfig({
|
|||||||
outputDir: "dist",
|
outputDir: "dist",
|
||||||
crossorigin: "anonymous",
|
crossorigin: "anonymous",
|
||||||
devServer: {
|
devServer: {
|
||||||
|
client: {
|
||||||
|
overlay: false // 关闭错误覆盖层
|
||||||
|
},
|
||||||
allowedHosts: "all",
|
allowedHosts: "all",
|
||||||
port: 8888,
|
port: 8888,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user