Skip to content

Commit

Permalink
feat: add chat mode
Browse files Browse the repository at this point in the history
  • Loading branch information
KeJunMao committed Mar 18, 2023
1 parent 3afed23 commit bf7ae4b
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 31 deletions.
1 change: 1 addition & 0 deletions components/create/CreateTheInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const rules = reactive(
maxlength="100"
show-word-limit
v-model="tool.desc"
:autosize="{ minRows: 3 }"
></el-input>
</el-form-item>
</el-form>
Expand Down
53 changes: 52 additions & 1 deletion components/create/CreateTheRole.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ const rules = reactive(
message: "Template is required",
},
],
chat: [
{
validator(_rule, value, cb) {
console.log(value);
if (
value &&
tool.value.roles[tool.value.roles.length - 1].type !== "user"
) {
cb(
"When chat mode is enabled, the last conversation must be is user."
);
}
cb();
},
trigger: "change",
},
],
})
);
Expand Down Expand Up @@ -44,10 +61,34 @@ function appendVariable(index: number, _var: string) {
tool.value.roles[index].template += `\$\{${_var}\}`;
}
}
const messageOrderWarning = computed(() => {
const roles = tool.value.roles;
const firstSystem = roles.find((v) => v.type === "system");
if (firstSystem && firstSystem !== roles[0]) {
return "Typically, a conversation is formatted with a system message first.";
} else if (
roles.length > 1 &&
roles.some((role, index, arr) => arr[index - 1]?.type === role.type)
) {
return "Typically, a conversation is alternating user and assistant messages.";
} else if (roles[roles.length - 1].type !== "user") {
return "Typically, the last conversation is user.";
}
});
</script>

<template>
<h3 mb-2 text text-gray>{{ $t("create.the-role.title") }}</h3>

<div pb-4 v-if="messageOrderWarning">
<el-alert
:title="messageOrderWarning"
:closable="false"
type="warning"
show-icon
/>
</div>
<CreateListTransition name="list">
<el-form :model="tool.roles" ref="formEl" label-position="top" size="large">
<div v-for="(item, index) in tool.roles" :key="item.id">
Expand Down Expand Up @@ -106,12 +147,22 @@ function appendVariable(index: number, _var: string) {
</el-tag>
</div>
<el-input
:label="$t('create.the-role.template.placeholder')"
:placeholder="$t('create.the-role.template.placeholder')"
type="textarea"
v-model="item.template"
:autosize="{ minRows: 3 }"
></el-input>
</el-form-item>
</div>
</el-form>
</CreateListTransition>
<el-form ref="formEl" :model="tool" label-position="top">
<el-form-item
prop="chat"
:label="$t('create.the-role.chat.label')"
:rules="rules.chat"
>
<el-switch v-model="tool.chat" />
</el-form-item>
</el-form>
</template>
59 changes: 59 additions & 0 deletions components/tool/ToolChatItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import { marked } from "marked";
const props = defineProps<{
role: string;
content: string;
showTyping?: boolean;
}>();
const resultHtml = computed(() => marked.parse(props.content || "..."));
</script>

<template>
<div>
<div flex gap-x-2 :class="role === 'user' ? 'flex-row-reverse' : ''">
<div>
<el-avatar class="bg-primary!">
<Icon
v-if="role === 'assistant'"
text-2xl
name="simple-icons:openai"
></Icon>
<Icon
v-else-if="role === 'user'"
text-2xl
name="carbon:user-filled"
></Icon>
</el-avatar>
</div>
<Card w-full relative>
<div text-base prose dark:prose-invert v-html="resultHtml"></div>
<transition name="fade">
<div
v-show="showTyping"
absolute
right-2
bottom-1
text-sm
text-gray
mt-2
>
OpenAI is typing...
</div>
</transition>
</Card>
</div>
</div>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
46 changes: 46 additions & 0 deletions components/tool/ToolChatResult.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ElScrollbar } from "element-plus";
import { OpenAIMessages } from "~~/types";
const props = defineProps<{
contexts: OpenAIMessages;
result: string;
loading: boolean;
}>();
const history = computed(() =>
props.contexts.filter((v) => v.role !== "system")
);
const scrollbar = ref<InstanceType<typeof ElScrollbar> | null>();
const innerRef = ref<HTMLDivElement>();
watch(
() => props.result,
() => {
nextTick(() => {
scrollbar.value?.setScrollTop(innerRef.value!.clientHeight);
});
}
);
</script>

<template>
<el-scrollbar ref="scrollbar" height="40vh" mb-8>
<div ref="innerRef" flex flex-col gap-y-6 px-4>
<el-empty v-if="!history.length" description="No chat data">
<template #image>
<el-icon
class="text-6xl! color-[var(--el-text-color-secondary)]! i-carbon:not-sent-filled"
></el-icon>
</template>
</el-empty>
<ToolChatItem v-for="item in history" v-bind="item"></ToolChatItem>
<ToolChatItem
v-if="loading"
show-typing
role="assistant"
:content="result"
></ToolChatItem>
</div>
</el-scrollbar>
</template>
29 changes: 23 additions & 6 deletions components/tool/ToolDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,40 @@ const props = defineProps<{
}>();
const { storageOptions } = useChatGPT();
const tool = computed(() => props.tool);
const { send, loading, result, resultHtml } = useAi(tool);
const { send, loading, result, resultHtml, cancel, contexts } = useAi(tool);
function submit(data: any) {
if (storageOptions.value.apiKey) {
send(data);
} else {
ElMessage.warning("Please set the API key first");
}
}
function stop() {
cancel();
}
</script>
<template>
<div>
<ToolHeader :tool="tool" />
<Card relative class="group">
<ToolActions :tool="tool" />
<ToolForms :loading="loading" @submit="submit" :tool="tool" />
<ToolResult v-if="result" :html="resultHtml" />
<Card relative class="group" px-0>
<template v-if="tool.chat">
<ToolChatResult
:contexts="contexts"
:result="result"
:loading="loading"
/>
<el-divider />
</template>
<div px-4>
<ToolActions :tool="tool" />
<ToolForms
:loading="loading"
@submit="submit"
@stop="stop"
:tool="tool"
/>
<ToolResult v-if="!tool.chat && result" :html="resultHtml" />
</div>
</Card>
</div>
</template>
16 changes: 14 additions & 2 deletions components/tool/ToolForms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,30 @@ export default defineComponent({
methods: {
submit() {
this.$emit("submit", this.formData);
// @ts-ignore
this.$refs.form?.resetFields()
},
stop() {
this.$emit("stop");
},
},
});
</script>
<template>
<el-form label-position="top" size="large">
<el-form :model="formData" ref="form" label-position="top" size="large">
<el-form-item
v-for="item in tool?.forms"
:key="item.name"
:label="item.lable"
:prop="item.name"
>
<component
w-full
:is="item.type"
v-model="formData[item.name]"
v-bind="item.props"
:readonly="readonly"
:autosize="{ minRows: 3 }"
>
<el-option
v-if="item.props.options && item.type === 'ElSelect'"
Expand All @@ -44,9 +51,14 @@ export default defineComponent({
</component>
</el-form-item>
<el-form-item>
<el-button :loading="loading" @click="submit" type="primary" w-full>
<el-button v-if="!loading" @click="submit" type="primary" w-full>
<el-icon class="text-xl! i-carbon:send-alt-filled mr-2"></el-icon>
{{ $t("tool.forms.submit") }}
</el-button>
<el-button v-else @click="stop" type="warning" w-full class="ml-0!">
<el-icon class="text-xl! i-carbon:stop-filled mr-2"></el-icon>
{{ $t("tool.forms.stop") }}
</el-button>
</el-form-item>
</el-form>
</template>
52 changes: 43 additions & 9 deletions composables/useAi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MaybeRef } from "@vueuse/core";
import { marked } from "marked";
import { ToolItem } from "~~/types";
import { OpenAIMessages, ToolItem } from "~~/types";
import { useChatGPT } from "./useChatGPT";

export const useAi = (_tool: MaybeRef<ToolItem>) => {
Expand All @@ -9,31 +9,65 @@ export const useAi = (_tool: MaybeRef<ToolItem>) => {
const result = ref("");
const loading = ref(false);
const resultHtml = computed(() => marked.parse(result.value));
const contexts = ref<OpenAIMessages>([]);
let controller = new AbortController();
let signal = controller.signal;

const send = async (data: MaybeRef<Record<string, any>>) => {
const tool = unref(_tool);
data = unref(data);
const messages = parseRoles(data, unref(_tool)?.roles!);
let messages = parseRoles(data, tool?.roles!);
if (tool.chat) {
if (!contexts.value.length) {
contexts.value.push(...messages);
} else {
contexts.value.push(messages[messages.length - 1]);
}
messages = contexts.value;
}
result.value = "";
loading.value = true;
try {
await sendMessage(
await sendMessage({
messages,
(message) => {
onProgress: (message) => {
result.value += message;
},
options.value
);
gptOptions: options.value,
signal,
});
} catch (error: any) {
result.value = error.data
? JSON.stringify(error.data, null, 2)
: null ?? error?.message ?? error;
if (error.message?.includes("The user aborted a request")) {
// pass
} else {
result.value = error.data
? JSON.stringify(error.data, null, 2)
: null ?? error?.message ?? error;
}
}
contexts.value.push({
content: result.value,
role: "assistant",
});
loading.value = false;
};
const reset = () => {
contexts.value = [];
result.value = "";
loading.value = false;
};
const cancel = () => {
controller.abort();
controller = new AbortController();
signal = controller.signal;
};
return {
send,
resultHtml,
result,
loading,
contexts,
cancel,
reset,
};
};
Loading

0 comments on commit bf7ae4b

Please sign in to comment.