mirror of
https://github.com/coaidev/coai.git
synced 2025-05-29 01:40:17 +09:00
implement conversation storage feature
This commit is contained in:
parent
76e43b1d3e
commit
f2be2ba82a
17
api/chat.go
17
api/chat.go
@ -14,6 +14,7 @@ import (
|
||||
|
||||
type WebsocketAuthForm struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
func SendSegmentMessage(conn *websocket.Conn, message types.ChatGPTSegmentResponse) {
|
||||
@ -62,7 +63,21 @@ func ChatAPI(c *gin.Context) {
|
||||
}
|
||||
|
||||
db := c.MustGet("db").(*sql.DB)
|
||||
instance := conversation.NewConversation(db, user.GetID(db))
|
||||
var instance *conversation.Conversation
|
||||
if form.Id == -1 {
|
||||
// create new conversation
|
||||
instance = conversation.NewConversation(db, user.GetID(db))
|
||||
} else {
|
||||
// load conversation
|
||||
instance = conversation.LoadConversation(db, user.GetID(db), form.Id)
|
||||
if instance == nil {
|
||||
SendSegmentMessage(conn, types.ChatGPTSegmentResponse{
|
||||
Message: "Conversation not found.",
|
||||
End: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
_, message, err = conn.ReadMessage()
|
||||
|
@ -81,5 +81,8 @@ func SearchWeb(message []types.ChatGPTMessage) string {
|
||||
Content: message[len(message)-1].Content,
|
||||
}}, 40)
|
||||
keyword := utils.UnmarshalJson[map[string]interface{}](resp)
|
||||
if keyword == nil {
|
||||
return ""
|
||||
}
|
||||
return StringCleaner(keyword["keyword"].(string))
|
||||
}
|
||||
|
136
app/src/App.vue
136
app/src/App.vue
@ -3,23 +3,20 @@ import Login from "./components/icons/login.vue";
|
||||
import { auth, username } from "./assets/script/auth";
|
||||
import Light from "./components/icons/light.vue";
|
||||
import Star from "./components/icons/star.vue";
|
||||
import { mobile, gpt4 } from "./assets/script/shared";
|
||||
import Post from "./components/icons/post.vue";
|
||||
import {mobile, gpt4, list, manager} from "./assets/script/shared";
|
||||
import Github from "./components/icons/github.vue";
|
||||
import Heart from "./components/icons/heart.vue";
|
||||
import Chat from "./components/icons/chat.vue";
|
||||
import Delete from "./components/icons/delete.vue";
|
||||
import { deleteConversation } from "./assets/script/api";
|
||||
|
||||
|
||||
const current = manager.getCurrent();
|
||||
function goto() {
|
||||
window.location.href = "https://deeptrain.net/login?app=chatnio";
|
||||
}
|
||||
|
||||
function toggle(n: boolean) {
|
||||
if (mobile.value) {
|
||||
gpt4.value = !gpt4.value;
|
||||
} else {
|
||||
gpt4.value = n;
|
||||
}
|
||||
|
||||
gpt4.value = mobile.value ? !gpt4.value : n;
|
||||
if (gpt4.value && !auth.value) return goto();
|
||||
}
|
||||
</script>
|
||||
@ -45,6 +42,18 @@ function toggle(n: boolean) {
|
||||
<heart />
|
||||
捐助我们
|
||||
</a>
|
||||
<div class="conversation-container" v-if="auth && !mobile">
|
||||
<div class="conversation"
|
||||
v-for="(conversation, idx) in list" :key="idx"
|
||||
:class="{'active': current === conversation.id}"
|
||||
@click="manager.toggle(conversation.id)"
|
||||
>
|
||||
<chat class="icon" />
|
||||
<div class="title">{{ conversation.name }}</div>
|
||||
<div class="id">{{ conversation.id + 1 }}</div>
|
||||
<delete class="delete" @click="deleteConversation(conversation.id)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div class="user" v-if="auth">
|
||||
<img class="avatar" :src="'https://api.deeptrain.net/avatar/' + username" alt="">
|
||||
@ -189,6 +198,115 @@ aside {
|
||||
stroke: rgb(255, 110, 122);
|
||||
}
|
||||
|
||||
.conversation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 235px;
|
||||
height: 100%;
|
||||
margin: 12px auto;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
gap: 8px;
|
||||
background: rgba(72, 73, 85, 0.1);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: calc(100% - 18px);
|
||||
height: 28px;
|
||||
padding: 8px 12px;
|
||||
margin: 0 16px;
|
||||
border-radius: 8px;
|
||||
background: rgba(152, 153, 165, 0.05);
|
||||
transition: .15s;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: center;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conversation:hover {
|
||||
background: rgba(152, 153, 165, 0.2);
|
||||
}
|
||||
|
||||
.conversation.active {
|
||||
background: rgba(152, 153, 165, 0.3);
|
||||
}
|
||||
|
||||
.conversation .icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
stroke: var(--card-text);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
margin-left: 4px;
|
||||
margin-right: 2px;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.conversation .title {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
color: var(--card-text);
|
||||
user-select: none;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.conversation .id::before {
|
||||
content: '#';
|
||||
}
|
||||
|
||||
.conversation .id {
|
||||
width: max-content;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
color: rgb(152, 153, 165);
|
||||
user-select: none;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.conversation:hover .id {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conversation .delete {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
stroke: rgb(182, 183, 205);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
margin-left: 4px;
|
||||
margin-right: 2px;
|
||||
transform: translateY(2px);
|
||||
display: none;
|
||||
transition: .25s;
|
||||
}
|
||||
|
||||
.conversation:hover .delete {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.model {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
32
app/src/assets/script/api.ts
Normal file
32
app/src/assets/script/api.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import axios from "axios";
|
||||
import {list} from "./shared";
|
||||
|
||||
type Message = {
|
||||
content: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export type ConversationInstance = {
|
||||
id: number;
|
||||
name: string;
|
||||
message?: Message[];
|
||||
}
|
||||
|
||||
export async function getConversationList(): Promise<ConversationInstance[]> {
|
||||
const resp = await axios.get("/conversation/list");
|
||||
if (resp.data.status) return resp.data.data as ConversationInstance[];
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function loadConversation(id: number): Promise<ConversationInstance> {
|
||||
const resp = await axios.get(`/conversation/load?id=${id}`);
|
||||
if (resp.data.status) return resp.data.data as ConversationInstance;
|
||||
return { id, name: "" };
|
||||
}
|
||||
|
||||
export async function deleteConversation(id: number): Promise<boolean> {
|
||||
const resp = await axios.get(`/conversation/delete?id=${id}`);
|
||||
if (!resp.data.status) return false;
|
||||
list.value = list.value.filter((item) => item.id !== id);
|
||||
return true;
|
||||
}
|
@ -5,7 +5,7 @@ import { auth, token } from "./auth";
|
||||
import { ws_api } from "./conf";
|
||||
import { gpt4 } from "./shared";
|
||||
|
||||
type Message = {
|
||||
export type Message = {
|
||||
content: string;
|
||||
role: string;
|
||||
time: string;
|
||||
@ -23,10 +23,12 @@ type StreamMessage = {
|
||||
export class Connection {
|
||||
protected connection: WebSocket | undefined;
|
||||
protected callback?: (message: StreamMessage) => void;
|
||||
public id: number;
|
||||
public state: boolean;
|
||||
|
||||
public constructor() {
|
||||
public constructor(id: number) {
|
||||
this.state = false;
|
||||
this.id = id;
|
||||
this.init();
|
||||
}
|
||||
|
||||
@ -37,6 +39,7 @@ export class Connection {
|
||||
this.state = true;
|
||||
this.send({
|
||||
token: token.value,
|
||||
id: this.id,
|
||||
})
|
||||
}
|
||||
this.connection.onclose = () => {
|
||||
@ -71,25 +74,40 @@ export class Connection {
|
||||
}
|
||||
export class Conversation {
|
||||
id: number;
|
||||
title: Ref<string>;
|
||||
messages: Message[];
|
||||
len: Ref<number>;
|
||||
state: Ref<boolean>;
|
||||
refresh: () => void;
|
||||
refresh?: () => void;
|
||||
connection: Connection | undefined;
|
||||
|
||||
public constructor(id: number, refresh: () => void) {
|
||||
public constructor(id: number, refresh?: () => void) {
|
||||
this.id = id;
|
||||
this.messages = reactive([]);
|
||||
this.state = ref(false);
|
||||
this.len = ref(0);
|
||||
this.refresh = refresh;
|
||||
if (auth.value) this.connection = new Connection();
|
||||
this.title = ref("new chat");
|
||||
if (auth.value) this.connection = new Connection(id);
|
||||
}
|
||||
|
||||
public setRefresh(refresh: () => void): void {
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
public setTitle(title: string): void {
|
||||
this.title.value = title;
|
||||
}
|
||||
|
||||
public notReady(): boolean {
|
||||
return Boolean(auth.value && !this.connection?.state);
|
||||
}
|
||||
|
||||
public setMessages(messages: Message[]): void {
|
||||
this.messages = reactive(messages);
|
||||
this.len = ref(messages.length);
|
||||
}
|
||||
|
||||
public async send(content: string): Promise<void> {
|
||||
if (this.notReady()) {
|
||||
const apply = () => {
|
||||
@ -125,7 +143,6 @@ export class Conversation {
|
||||
this.addMessageFromUser(content);
|
||||
try {
|
||||
const res = await axios.post("/anonymous", {
|
||||
"id": this.id,
|
||||
"message": content,
|
||||
});
|
||||
if (res.data.status === true) {
|
||||
@ -151,7 +168,7 @@ export class Conversation {
|
||||
gpt4: gpt4.value,
|
||||
})
|
||||
nextTick(() => {
|
||||
this.refresh();
|
||||
this.refresh && this.refresh();
|
||||
}).then(r => 0);
|
||||
}
|
||||
|
||||
@ -184,7 +201,7 @@ export class Conversation {
|
||||
const interval = setInterval(() => {
|
||||
this.messages[index].content = content.substring(0, cursor);
|
||||
cursor++;
|
||||
this.refresh();
|
||||
this.refresh && this.refresh();
|
||||
if (cursor > content.length) {
|
||||
this.state.value = false;
|
||||
clearInterval(interval);
|
||||
@ -205,10 +222,14 @@ export class Conversation {
|
||||
if (cursor >= content.value.length) return;
|
||||
cursor++;
|
||||
this.messages[index].content = content.value.substring(0, cursor);
|
||||
this.refresh();
|
||||
this.refresh && this.refresh();
|
||||
}, 20);
|
||||
}
|
||||
|
||||
public getTitle(): Ref<string> {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public getMessages(): Message[] {
|
||||
return this.messages;
|
||||
}
|
||||
|
140
app/src/assets/script/manager.ts
Normal file
140
app/src/assets/script/manager.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import {nextTick, reactive, ref, watch} from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import {Conversation} from "./conversation";
|
||||
import type { Message } from "./conversation";
|
||||
import {loadConversation} from "./api";
|
||||
import {auth} from "./auth";
|
||||
|
||||
function convert(message: { content: string, role: string }): Message {
|
||||
return {
|
||||
content: message.content,
|
||||
role: message.role,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
stamp: new Date().getTime(),
|
||||
} as Message;
|
||||
}
|
||||
|
||||
export class Manager {
|
||||
public current: Ref<number>;
|
||||
public title: Ref<string>;
|
||||
public state: Ref<boolean>;
|
||||
public length: Ref<number>;
|
||||
public messages: Message[];
|
||||
public refresh?: () => void;
|
||||
conversations: Record<number, Conversation>
|
||||
|
||||
public constructor(refresh?: () => void) {
|
||||
this.refresh = refresh;
|
||||
this.conversations = {};
|
||||
this.current = ref(NaN);
|
||||
this.title = ref("new chat");
|
||||
this.state = ref(false);
|
||||
this.length = ref(0);
|
||||
this.messages = reactive([]);
|
||||
|
||||
watch(auth, () => {
|
||||
this.init();
|
||||
})
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.conversations[-1] = new Conversation(-1, this.refresh);
|
||||
this.set(-1);
|
||||
}
|
||||
|
||||
public setRefresh(refresh: () => void): void {
|
||||
this.refresh = refresh;
|
||||
for (const conversation in this.conversations) this.conversations[conversation].setRefresh(refresh);
|
||||
}
|
||||
|
||||
protected refreshMessages(message: Message[]): void {
|
||||
this.messages = message;
|
||||
}
|
||||
|
||||
protected refreshState(state: Ref<boolean>): void {
|
||||
this.state.value = state.value;
|
||||
watch(state, () => {
|
||||
this.state.value = this.conversations[this.current.value].getState().value;
|
||||
})
|
||||
}
|
||||
|
||||
protected refreshLength(length: Ref<number>): void {
|
||||
this.length.value = length.value;
|
||||
watch(length, () => {
|
||||
this.length.value = this.conversations[this.current.value].getLength().value;
|
||||
})
|
||||
}
|
||||
|
||||
protected refreshTitle(title: Ref<string>): void {
|
||||
this.title.value = title.value;
|
||||
watch(title, () => {
|
||||
this.title.value = this.conversations[this.current.value].getTitle().value;
|
||||
})
|
||||
}
|
||||
|
||||
public set(id: number): void {
|
||||
if (this.current.value === id) return;
|
||||
this.current.value = id;
|
||||
if (!this.conversations[id]) this.conversations[id] = new Conversation(id, this.refresh);
|
||||
this.refreshState(this.conversations[id].getState());
|
||||
this.refreshLength(this.conversations[id].getLength());
|
||||
this.refreshTitle(this.conversations[id].getTitle());
|
||||
this.refreshMessages(this.conversations[id].getMessages());
|
||||
|
||||
nextTick(() => {
|
||||
this.refresh && this.refresh();
|
||||
}).then();
|
||||
}
|
||||
|
||||
public getState(): Ref<boolean> {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public getLength(): Ref<number> {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public getMessages(): Message[] {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
public getConversations(): Record<number, Conversation> {
|
||||
return this.conversations;
|
||||
}
|
||||
|
||||
public getTitle(): Ref<string> {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public getCurrent(): Ref<number> {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
public async add(id: number): Promise<void> {
|
||||
if (this.conversations[id]) return;
|
||||
const instance = new Conversation(id, this.refresh);
|
||||
this.conversations[id] = instance;
|
||||
const res = await loadConversation(id);
|
||||
instance.setMessages(res.message?.map(convert) || []);
|
||||
instance.setTitle(res.name);
|
||||
}
|
||||
|
||||
public async toggle(id: number): Promise<void> {
|
||||
if (this.current.value === id) return;
|
||||
if (!this.conversations[id]) await this.add(id);
|
||||
this.set(id);
|
||||
}
|
||||
|
||||
public delete(id: number): void {
|
||||
if (this.current.value === id) this.set(-1);
|
||||
delete this.conversations[id];
|
||||
}
|
||||
|
||||
public async send(content: string): Promise<void> {
|
||||
await this.conversations[this.current.value].send(content);
|
||||
}
|
||||
|
||||
public get(id: number): Conversation {
|
||||
return this.conversations[id];
|
||||
}
|
||||
}
|
@ -1,8 +1,18 @@
|
||||
import {ref} from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import type { ConversationInstance } from "./api";
|
||||
import { auth } from "./auth";
|
||||
import { getConversationList } from "./api";
|
||||
import {Manager} from "./manager";
|
||||
|
||||
export const mobile = ref<boolean>((document.body.clientWidth < document.body.clientHeight) && (document.body.clientWidth < 600));
|
||||
export const gpt4 = ref(false);
|
||||
|
||||
export const list = ref<ConversationInstance[]>([]);
|
||||
export const manager = new Manager();
|
||||
window.addEventListener("resize", () => {
|
||||
mobile.value = (document.body.clientWidth < document.body.clientHeight) && (document.body.clientWidth < 600);
|
||||
})
|
||||
|
||||
watch(auth, async () => {
|
||||
if (!auth.value) return;
|
||||
list.value = await getConversationList();
|
||||
});
|
||||
|
3
app/src/components/icons/chat.vue
Normal file
3
app/src/components/icons/chat.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||
</template>
|
3
app/src/components/icons/delete.vue
Normal file
3
app/src/components/icons/delete.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
</template>
|
@ -3,14 +3,21 @@ import 'md-editor-v3/lib/style.css';
|
||||
import Post from "../components/icons/post.vue";
|
||||
import Openai from "../components/icons/openai.vue";
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
import {Conversation} from "../assets/script/conversation";
|
||||
import {nextTick, onMounted, ref} from "vue";
|
||||
import {auth, username} from "../assets/script/auth";
|
||||
import {nextTick, onMounted, ref, watch} from "vue";
|
||||
import { auth, username } from "../assets/script/auth";
|
||||
import Loading from "../components/icons/loading.vue";
|
||||
import Bing from "../components/icons/bing.vue";
|
||||
import { manager } from "../assets/script/shared";
|
||||
|
||||
const state = manager.getState(), length = manager.getLength(), current = manager.getCurrent();
|
||||
manager.setRefresh(function refreshScrollbar() {
|
||||
nextTick(() => {
|
||||
if (!chatEl.value) return;
|
||||
const el = chatEl.value as HTMLElement;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
})
|
||||
});
|
||||
|
||||
const conversation = new Conversation(1, refreshScrollbar);
|
||||
const state = conversation.getState(), length = conversation.getLength(), messages = conversation.getMessages();
|
||||
const input = ref("");
|
||||
const inputEl = ref<HTMLElement | undefined>();
|
||||
const chatEl = ref<HTMLElement | undefined>();
|
||||
@ -19,18 +26,10 @@ async function send() {
|
||||
let val = input.value.trim();
|
||||
if (val && !state.value) {
|
||||
input.value = "";
|
||||
await conversation.send(val);
|
||||
await manager.send(val);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshScrollbar() {
|
||||
nextTick(() => {
|
||||
if (!chatEl.value) return;
|
||||
const el = chatEl.value as HTMLElement;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!inputEl.value) return;
|
||||
const param = new URLSearchParams(window.location.search);
|
||||
@ -50,27 +49,29 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="chat-wrapper" ref="chatEl">
|
||||
<div class="conversation" v-if="length">
|
||||
<template v-for="(message, index) in messages" :key="index">
|
||||
<div class="time" v-if="index === 0 || message.stamp - messages[index - 1].stamp > 10 * 60 * 1000">
|
||||
{{ message.time }}
|
||||
</div>
|
||||
<div class="message" :class="{'user': message.role === 'user'}">
|
||||
<div class="grow" v-if="message.role === 'user'"></div>
|
||||
<div class="avatar openai" :class="{'gpt4': message.gpt4}" v-else><openai /></div>
|
||||
<div class="content">
|
||||
<div v-if="message.role === 'bot' && message.keyword !== ''" class="bing">
|
||||
<bing />
|
||||
{{ message.keyword }}
|
||||
<template v-for="(conversation, i, k) in manager.conversations" :key="k">
|
||||
<template v-for="(message, index) in conversation.messages" :key="index" v-if="current === conversation.id">
|
||||
<div class="time" v-if="index === 0 || message.stamp - conversation.messages[index - 1].stamp > 10 * 60 * 1000">
|
||||
{{ message.time }}
|
||||
</div>
|
||||
<div class="message" :class="{'user': message.role === 'user'}">
|
||||
<div class="grow" v-if="message.role === 'user'"></div>
|
||||
<div class="avatar openai" :class="{'gpt4': message.gpt4}" v-else><openai /></div>
|
||||
<div class="content">
|
||||
<div v-if="message.role === 'bot' && message.keyword?.trim() !== ''" class="bing">
|
||||
<bing />
|
||||
{{ message.keyword }}
|
||||
</div>
|
||||
<div class="loader" v-if="!message.content" />
|
||||
<span v-if="message.role === 'user'">{{ message.content }}</span>
|
||||
<md-preview v-model="message.content" theme="dark" v-else />
|
||||
</div>
|
||||
<div class="avatar user" v-if="message.role === 'user'">
|
||||
<img :src="'https://api.deeptrain.net/avatar/' + username" alt="" v-if="auth">
|
||||
<img src="/favicon.ico" alt="" v-else>
|
||||
</div>
|
||||
<div class="loader" v-if="!message.content" />
|
||||
<span v-if="message.role === 'user'">{{ message.content }}</span>
|
||||
<md-preview v-model="message.content" theme="dark" v-else />
|
||||
</div>
|
||||
<div class="avatar user" v-if="message.role === 'user'">
|
||||
<img :src="'https://api.deeptrain.net/avatar/' + username" alt="" v-if="auth">
|
||||
<img src="/favicon.ico" alt="" v-else>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="preview" v-else>
|
||||
|
@ -24,3 +24,16 @@ func Middleware() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GetToken(c *gin.Context) string {
|
||||
return c.GetString("token")
|
||||
}
|
||||
|
||||
func GetUser(c *gin.Context) *User {
|
||||
if c.GetBool("auth") {
|
||||
return &User{
|
||||
Username: c.GetString("user"),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
96
conversation/api.go
Normal file
96
conversation/api.go
Normal file
@ -0,0 +1,96 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"chat/auth"
|
||||
"database/sql"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ListAPI(c *gin.Context) {
|
||||
user := auth.GetUser(c)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "user not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := c.MustGet("db").(*sql.DB)
|
||||
conversations := LoadConversationList(db, user.GetID(db))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"message": "",
|
||||
"data": conversations,
|
||||
})
|
||||
}
|
||||
|
||||
func LoadAPI(c *gin.Context) {
|
||||
user := auth.GetUser(c)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "user not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := c.MustGet("db").(*sql.DB)
|
||||
id, err := strconv.ParseInt(c.Query("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "invalid id",
|
||||
})
|
||||
return
|
||||
}
|
||||
conversation := LoadConversation(db, user.GetID(db), id)
|
||||
if conversation == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "conversation not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"message": "",
|
||||
"data": conversation,
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteAPI(c *gin.Context) {
|
||||
user := auth.GetUser(c)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "user not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := c.MustGet("db").(*sql.DB)
|
||||
id, err := strconv.ParseInt(c.Query("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "invalid id",
|
||||
})
|
||||
return
|
||||
}
|
||||
conversation := LoadConversation(db, user.GetID(db), id)
|
||||
if conversation == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "conversation not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
conversation.DeleteConversation(db)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
@ -21,7 +21,7 @@ type FormMessage struct {
|
||||
func NewConversation(db *sql.DB, id int64) *Conversation {
|
||||
return &Conversation{
|
||||
UserID: id,
|
||||
Id: GetConversationLengthByUserID(db, id),
|
||||
Id: GetConversationLengthByUserID(db, id) + 1,
|
||||
Name: "new chat",
|
||||
Message: []types.ChatGPTMessage{},
|
||||
}
|
||||
@ -120,11 +120,14 @@ func (c *Conversation) AddMessageFromUserForm(data []byte) (string, error) {
|
||||
}
|
||||
|
||||
func (c *Conversation) HandleMessage(db *sql.DB, data []byte) bool {
|
||||
_, err := c.AddMessageFromUserForm(data)
|
||||
head := len(c.Message) == 0
|
||||
msg, err := c.AddMessageFromUserForm(data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if head {
|
||||
c.SetName(db, msg)
|
||||
}
|
||||
c.SaveConversation(db)
|
||||
return true
|
||||
}
|
||||
|
@ -51,7 +51,12 @@ func LoadConversationList(db *sql.DB, userId int64) []Conversation {
|
||||
if err != nil {
|
||||
return conversationList
|
||||
}
|
||||
defer rows.Close()
|
||||
defer func(rows *sql.Rows) {
|
||||
err := rows.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(rows)
|
||||
|
||||
for rows.Next() {
|
||||
var conversation Conversation
|
||||
@ -62,5 +67,13 @@ func LoadConversationList(db *sql.DB, userId int64) []Conversation {
|
||||
conversationList = append(conversationList, conversation)
|
||||
}
|
||||
|
||||
return conversationList
|
||||
return utils.Reverse(conversationList)
|
||||
}
|
||||
|
||||
func (c *Conversation) DeleteConversation(db *sql.DB) bool {
|
||||
_, err := db.Exec("DELETE FROM conversation WHERE user_id = ? AND conversation_id = ?", c.UserID, c.Id)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
5
main.go
5
main.go
@ -4,6 +4,7 @@ import (
|
||||
"chat/api"
|
||||
"chat/auth"
|
||||
"chat/connection"
|
||||
"chat/conversation"
|
||||
"chat/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
@ -26,7 +27,9 @@ func main() {
|
||||
app.GET("/chat", api.ChatAPI)
|
||||
app.POST("/login", auth.LoginAPI)
|
||||
app.POST("/state", auth.StateAPI)
|
||||
|
||||
app.GET("/conversation/list", conversation.ListAPI)
|
||||
app.GET("/conversation/load", conversation.LoadAPI)
|
||||
app.GET("/conversation/delete", conversation.DeleteAPI)
|
||||
}
|
||||
if viper.GetBool("debug") {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
|
@ -85,3 +85,10 @@ func GetLatestSegment[T any](arr []T, length int) []T {
|
||||
}
|
||||
return arr[len(arr)-length:]
|
||||
}
|
||||
|
||||
func Reverse[T any](arr []T) []T {
|
||||
for i := 0; i < len(arr)/2; i++ {
|
||||
arr[i], arr[len(arr)-i-1] = arr[len(arr)-i-1], arr[i]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user