mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 21:40:15 +09:00
restruct: restruct conversation and history runtime and dispatch
This commit is contained in:
parent
67cb512eb4
commit
43a2cbfe1e
@ -1,4 +1,4 @@
|
||||
import { Model } from "@/api/types.ts";
|
||||
import { Model } from "@/api/types.tsx";
|
||||
import { CommonResponse } from "@/api/common.ts";
|
||||
import axios from "axios";
|
||||
import { getErrorMessage } from "@/utils/base.ts";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Plan } from "@/api/types";
|
||||
import { Plan } from "@/api/types.tsx";
|
||||
import axios from "axios";
|
||||
import { CommonResponse } from "@/api/common.ts";
|
||||
import { getErrorMessage } from "@/utils/base.ts";
|
||||
|
@ -3,7 +3,7 @@ import { getUniqueList } from "@/utils/base.ts";
|
||||
import { defaultChannelModels } from "@/admin/channel.ts";
|
||||
import { getApiMarket, getApiModels } from "@/api/v1.ts";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { Model } from "@/api/types.ts";
|
||||
import { Model } from "@/api/types.tsx";
|
||||
|
||||
export type onStateChange<T> = (state: boolean, data?: T) => void;
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Mask } from "@/masks/types.ts";
|
||||
|
||||
export const endpoint = `${websocketEndpoint}/chat`;
|
||||
export const maxRetry = 60; // 30s max websocket retry
|
||||
export const maxConnection = 5;
|
||||
|
||||
export type StreamMessage = {
|
||||
conversation?: number;
|
||||
@ -37,14 +38,15 @@ type StreamCallback = (id: number, message: StreamMessage) => void;
|
||||
export class Connection {
|
||||
protected connection?: WebSocket;
|
||||
protected callback?: StreamCallback;
|
||||
protected stack?: string;
|
||||
protected stack?: Record<string, any>;
|
||||
public id: number;
|
||||
public state: boolean;
|
||||
|
||||
public constructor(id: number, callback?: StreamCallback) {
|
||||
this.state = false;
|
||||
this.id = id;
|
||||
this.callback && this.setCallback(callback);
|
||||
|
||||
callback && this.setCallback(callback);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
@ -57,8 +59,16 @@ export class Connection {
|
||||
id: this.id,
|
||||
});
|
||||
};
|
||||
this.connection.onclose = () => {
|
||||
this.connection.onclose = (event) => {
|
||||
this.state = false;
|
||||
|
||||
this.stack = {
|
||||
error: "websocket connection failed",
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
endpoint: endpoint,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
console.debug(`[connection] reconnecting... (id: ${this.id})`);
|
||||
this.init();
|
||||
@ -68,10 +78,6 @@ export class Connection {
|
||||
const message = JSON.parse(event.data);
|
||||
this.triggerCallback(message as StreamMessage);
|
||||
};
|
||||
|
||||
this.connection.onclose = (event) => {
|
||||
this.stack = `websocket connection failed (code: ${event.code}, reason: ${event.reason}, endpoint: ${endpoint})`;
|
||||
};
|
||||
}
|
||||
|
||||
public reconnect(): void {
|
||||
@ -105,16 +111,15 @@ export class Connection {
|
||||
);
|
||||
}
|
||||
|
||||
const trace =
|
||||
this.stack ||
|
||||
JSON.stringify(
|
||||
{
|
||||
message: data.message,
|
||||
endpoint: endpoint,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
const trace = JSON.stringify(
|
||||
this.stack ?? {
|
||||
message: data.message,
|
||||
endpoint: endpoint,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
this.stack = undefined;
|
||||
|
||||
t &&
|
||||
this.triggerCallback({
|
||||
@ -171,6 +176,16 @@ export class Connection {
|
||||
public setId(id: number): void {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public isReady(): boolean {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public isRunning(): boolean {
|
||||
if (!this.connection || !this.state) return false;
|
||||
|
||||
return this.connection.readyState === WebSocket.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionStack {
|
||||
@ -186,12 +201,30 @@ export class ConnectionStack {
|
||||
return this.connections.find((conn) => conn.id === id);
|
||||
}
|
||||
|
||||
public addConnection(id: number): Connection {
|
||||
public createConnection(id: number): Connection {
|
||||
const conn = new Connection(id, this.triggerCallback.bind(this));
|
||||
this.connections.push(conn);
|
||||
|
||||
// max connection garbage collection
|
||||
if (this.connections.length > maxConnection) {
|
||||
const garbage = this.connections.shift();
|
||||
garbage && garbage.close();
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
public send(id: number, t: any, props: ChatProps) {
|
||||
const conn = this.getConnection(id);
|
||||
if (!conn) return false;
|
||||
|
||||
conn.sendWithRetry(t, props);
|
||||
return true;
|
||||
}
|
||||
|
||||
public hasConnection(id: number): boolean {
|
||||
return this.connections.some((conn) => conn.id === id);
|
||||
}
|
||||
|
||||
public setCallback(callback?: StreamCallback): void {
|
||||
this.callback = callback;
|
||||
}
|
||||
@ -216,9 +249,9 @@ export class ConnectionStack {
|
||||
conn && conn.sendMaskEvent(t, mask);
|
||||
}
|
||||
|
||||
public sendEditEvent(id: number, t: any, message: string) {
|
||||
public sendEditEvent(id: number, t: any, messageId: number, message: string) {
|
||||
const conn = this.getConnection(id);
|
||||
conn && conn.sendEditEvent(t, id, message);
|
||||
conn && conn.sendEditEvent(t, messageId, message);
|
||||
}
|
||||
|
||||
public sendRemoveEvent(id: number, t: any, messageId: number) {
|
||||
@ -249,6 +282,13 @@ export class ConnectionStack {
|
||||
this.connections.forEach((conn) => conn.reconnect());
|
||||
}
|
||||
|
||||
public raiseConnection(id: number): void {
|
||||
const conn = this.getConnection(-1);
|
||||
if (!conn) return;
|
||||
|
||||
conn.setId(id);
|
||||
}
|
||||
|
||||
public triggerCallback(id: number, message: StreamMessage): void {
|
||||
this.callback && this.callback(id, message);
|
||||
}
|
||||
|
@ -1,300 +0,0 @@
|
||||
import { ChatProps, Connection, StreamMessage } from "./connection.ts";
|
||||
import { Message } from "./types.ts";
|
||||
import { sharingEvent } from "@/events/sharing.ts";
|
||||
import { connectionEvent } from "@/events/connection.ts";
|
||||
import { AppDispatch } from "@/store";
|
||||
import { setMessages } from "@/store/chat.ts";
|
||||
import { modelEvent } from "@/events/model.ts";
|
||||
import { Mask } from "@/masks/types.ts";
|
||||
|
||||
type ConversationCallback = (idx: number, message: Message[]) => boolean;
|
||||
|
||||
export type ConversationSerialized = {
|
||||
model: string;
|
||||
end: boolean;
|
||||
mask: Mask | null;
|
||||
messages: Message[];
|
||||
};
|
||||
|
||||
export class Conversation {
|
||||
protected connection?: Connection;
|
||||
protected callback?: ConversationCallback;
|
||||
protected idx: number;
|
||||
public id: number;
|
||||
public model: string;
|
||||
public data: Message[];
|
||||
public end: boolean;
|
||||
public mask: Mask | null;
|
||||
|
||||
public constructor(id: number, callback?: ConversationCallback) {
|
||||
if (callback) this.setCallback(callback);
|
||||
this.data = [];
|
||||
this.idx = -1;
|
||||
this.id = id;
|
||||
this.model = "";
|
||||
this.end = true;
|
||||
this.mask = null;
|
||||
this.connection = new Connection(this.id);
|
||||
|
||||
if (id === -1 && this.idx === -1) {
|
||||
sharingEvent.bind(({ refer, data }) => {
|
||||
console.debug(
|
||||
`[conversation] load from sharing event (ref: ${refer}, length: ${data.length})`,
|
||||
);
|
||||
|
||||
this.load(data);
|
||||
this.sendShareEvent(refer);
|
||||
});
|
||||
}
|
||||
|
||||
connectionEvent.addEventListener((ev) => {
|
||||
if (ev.id === this.id) {
|
||||
console.debug(
|
||||
`[conversation] connection event (id: ${this.id}, event: ${ev.event})`,
|
||||
);
|
||||
|
||||
switch (ev.event) {
|
||||
case "stop":
|
||||
this.end = true;
|
||||
this.data[this.data.length - 1].end = true;
|
||||
this.sendStopEvent();
|
||||
this.triggerCallback();
|
||||
break;
|
||||
|
||||
case "restart":
|
||||
this.end = false;
|
||||
delete this.data[this.data.length - 1];
|
||||
this.connection?.setCallback(this.useMessage());
|
||||
this.sendRestartEvent();
|
||||
break;
|
||||
|
||||
case "edit":
|
||||
const index = ev.index ?? -1;
|
||||
const message = ev.message ?? "";
|
||||
|
||||
if (this.isValidIndex(index)) {
|
||||
this.data[index].content = message;
|
||||
this.sendEditEvent(index, message);
|
||||
this.triggerCallback();
|
||||
}
|
||||
break;
|
||||
|
||||
case "remove":
|
||||
const idx = ev.index ?? -1;
|
||||
|
||||
if (this.isValidIndex(idx)) {
|
||||
this.data.splice(idx, 1);
|
||||
|
||||
this.sendRemoveEvent(idx);
|
||||
this.triggerCallback();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.debug(
|
||||
`[conversation] unknown event: ${ev.event} (from: ${ev.id})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected sendEvent(event: string, data?: string) {
|
||||
this.connection?.sendWithRetry(null, {
|
||||
type: event,
|
||||
message: data || "",
|
||||
model: "event",
|
||||
});
|
||||
}
|
||||
|
||||
public isValidIndex(idx: number): boolean {
|
||||
return idx >= 0 && idx < this.data.length;
|
||||
}
|
||||
|
||||
public sendStopEvent() {
|
||||
this.sendEvent("stop");
|
||||
}
|
||||
|
||||
public sendRestartEvent() {
|
||||
this.sendEvent("restart");
|
||||
}
|
||||
|
||||
public sendMaskEvent(mask: Mask) {
|
||||
this.sendEvent("mask", JSON.stringify(mask.context));
|
||||
}
|
||||
|
||||
public sendEditEvent(id: number, message: string) {
|
||||
this.sendEvent("edit", `${id}:${message}`);
|
||||
}
|
||||
|
||||
public sendRemoveEvent(id: number) {
|
||||
this.sendEvent("remove", id.toString());
|
||||
}
|
||||
|
||||
public sendShareEvent(refer: string) {
|
||||
this.sendEvent("share", refer);
|
||||
}
|
||||
|
||||
public preflightMask(mask: Mask) {
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public presentMask() {
|
||||
if (this.mask) {
|
||||
this.sendMaskEvent(this.mask);
|
||||
this.mask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public setId(id: number): void {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public copyMessages(): Message[] {
|
||||
// deep copy: cannot use return [...this.data];
|
||||
return this.data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public load(data: Message[]): void {
|
||||
this.data = data;
|
||||
this.idx = data.length - 1;
|
||||
this.triggerCallback();
|
||||
}
|
||||
|
||||
public getLength(): number {
|
||||
return this.data.length;
|
||||
}
|
||||
|
||||
public getIndex(): number {
|
||||
return this.idx;
|
||||
}
|
||||
|
||||
public getMessage(idx: number): Message {
|
||||
if (idx < 0 || idx >= this.getLength()) {
|
||||
throw new Error("Index out of range");
|
||||
}
|
||||
return this.data[idx];
|
||||
}
|
||||
|
||||
public setCallback(callback: ConversationCallback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public setModel(model?: string) {
|
||||
if (!model) return;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public getModel(): string {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.getLength() === 0;
|
||||
}
|
||||
|
||||
public toggle(dispatch: AppDispatch): void {
|
||||
dispatch(setMessages(this.copyMessages()));
|
||||
modelEvent.emit(this.getModel());
|
||||
}
|
||||
|
||||
public triggerCallback() {
|
||||
if (!this.callback) return;
|
||||
const interval = setInterval(() => {
|
||||
const state =
|
||||
this.callback && this.callback(this.id, this.copyMessages());
|
||||
if (state) clearInterval(interval);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public addMessage(message: Message): number {
|
||||
this.idx++;
|
||||
this.data.push(message);
|
||||
this.triggerCallback();
|
||||
return this.idx;
|
||||
}
|
||||
|
||||
public setMessage(idx: number, message: Message) {
|
||||
this.data[idx] = message;
|
||||
this.triggerCallback();
|
||||
}
|
||||
|
||||
public updateMessage(
|
||||
idx: number,
|
||||
message: string,
|
||||
keyword?: string,
|
||||
quota?: number,
|
||||
end?: boolean,
|
||||
plan?: boolean,
|
||||
) {
|
||||
this.data[idx].content += message;
|
||||
if (keyword) this.data[idx].keyword = keyword;
|
||||
if (quota) this.data[idx].quota = quota;
|
||||
this.data[idx].end = end;
|
||||
this.data[idx].plan = plan;
|
||||
this.triggerCallback();
|
||||
}
|
||||
|
||||
public useMessage(): (message: StreamMessage) => void {
|
||||
const cursor = this.addMessage({
|
||||
content: "",
|
||||
role: "assistant",
|
||||
});
|
||||
|
||||
return (message: StreamMessage) => {
|
||||
this.updateMessage(
|
||||
cursor,
|
||||
message.message,
|
||||
message.keyword,
|
||||
message.quota,
|
||||
message.end,
|
||||
message.plan,
|
||||
);
|
||||
if (message.end) {
|
||||
this.end = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public send(t: any, props: ChatProps) {
|
||||
if (!this.connection) {
|
||||
this.connection = new Connection(this.id);
|
||||
}
|
||||
this.end = false;
|
||||
this.connection.setCallback(this.useMessage()); // hook
|
||||
this.connection.sendWithRetry(t, props);
|
||||
}
|
||||
|
||||
public sendMessage(t: any, props: ChatProps): boolean {
|
||||
if (!this.end) return false;
|
||||
|
||||
this.presentMask();
|
||||
this.addMessage({
|
||||
content: props.message,
|
||||
role: "user",
|
||||
});
|
||||
|
||||
this.send(t, props);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public sendMessageWithRaise(t: any, id: number, props: ChatProps): boolean {
|
||||
if (!this.end) return false;
|
||||
|
||||
this.presentMask();
|
||||
this.addMessage({
|
||||
content: props.message,
|
||||
role: "user",
|
||||
});
|
||||
|
||||
this.send(t, props);
|
||||
this.setId(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import axios from "axios";
|
||||
import type { ConversationInstance } from "./types.ts";
|
||||
import type { ConversationInstance } from "./types.tsx";
|
||||
import { setHistory } from "@/store/chat.ts";
|
||||
import { manager } from "./manager.ts";
|
||||
import { AppDispatch } from "@/store";
|
||||
|
||||
export async function getConversationList(): Promise<ConversationInstance[]> {
|
||||
@ -37,38 +36,22 @@ export async function loadConversation(
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteConversation(
|
||||
dispatch: AppDispatch,
|
||||
id: number,
|
||||
): Promise<boolean> {
|
||||
export async function deleteConversation(id: number): Promise<boolean> {
|
||||
try {
|
||||
const resp = await axios.get(`/conversation/delete?id=${id}`);
|
||||
if (!resp.data.status) return false;
|
||||
await manager.delete(dispatch, id);
|
||||
return true;
|
||||
return resp.data.status;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAllConversations(
|
||||
dispatch: AppDispatch,
|
||||
): Promise<boolean> {
|
||||
export async function deleteAllConversations(): Promise<boolean> {
|
||||
try {
|
||||
const resp = await axios.get("/conversation/clean");
|
||||
if (!resp.data.status) return false;
|
||||
await manager.deleteAll(dispatch);
|
||||
return true;
|
||||
return resp.data.status;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleConversation(
|
||||
dispatch: AppDispatch,
|
||||
id: number,
|
||||
): Promise<void> {
|
||||
return manager.toggle(dispatch, id);
|
||||
}
|
||||
|
@ -1,173 +0,0 @@
|
||||
import { Conversation } from "@/api/conversation.ts";
|
||||
import { ConversationMapper, Message } from "@/api/types.ts";
|
||||
import { loadConversation } from "@/api/history.ts";
|
||||
import {
|
||||
addHistory,
|
||||
removeHistory,
|
||||
setCurrent,
|
||||
setMessages,
|
||||
} from "@/store/chat.ts";
|
||||
import { useShared } from "@/utils/hook.ts";
|
||||
import { ChatProps } from "@/api/connection.ts";
|
||||
import { AppDispatch } from "@/store";
|
||||
import { sharingEvent } from "@/events/sharing.ts";
|
||||
import { maskEvent } from "@/events/mask.ts";
|
||||
import { Mask } from "@/masks/types.ts";
|
||||
|
||||
export class Manager {
|
||||
conversations: Record<number, Conversation>;
|
||||
current: number;
|
||||
dispatch?: AppDispatch;
|
||||
|
||||
public constructor() {
|
||||
this.conversations = {};
|
||||
this.conversations[-1] = this.createConversation(-1);
|
||||
this.current = -1;
|
||||
|
||||
const _this = this;
|
||||
sharingEvent.addEventListener(async (data) => {
|
||||
console.debug(`[manager] accept sharing event (refer: ${data.refer})`);
|
||||
await _this.newPage();
|
||||
});
|
||||
|
||||
maskEvent.addEventListener(async (mask) => {
|
||||
console.debug(`[manager] accept mask event (name: ${mask.name})`);
|
||||
await _this.newMaskPage(mask);
|
||||
});
|
||||
}
|
||||
|
||||
public setDispatch(dispatch: AppDispatch): void {
|
||||
this.dispatch = dispatch;
|
||||
}
|
||||
|
||||
public async newPage(): Promise<void> {
|
||||
const interval = setInterval(() => {
|
||||
if (this.dispatch) {
|
||||
this.toggle(this.dispatch, -1);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public async newMaskPage(mask: Mask): Promise<void> {
|
||||
const interval = setInterval(() => {
|
||||
if (this.dispatch) {
|
||||
this.toggle(this.dispatch, -1);
|
||||
|
||||
const instance = this.get(-1);
|
||||
if (!instance) return;
|
||||
|
||||
instance.load([...mask.context]); // deep copy
|
||||
instance.preflightMask(mask);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public callback(idx: number, message: Message[]): boolean {
|
||||
console.debug(
|
||||
`[manager] conversation receive message (id: ${idx}, length: ${message.length})`,
|
||||
);
|
||||
if (idx === this.current) this.dispatch?.(setMessages(message));
|
||||
return !!this.dispatch;
|
||||
}
|
||||
|
||||
public getCurrent(): number {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
public getConversations(): ConversationMapper {
|
||||
return this.conversations;
|
||||
}
|
||||
|
||||
public createConversation(id: number): Conversation {
|
||||
console.debug(`[manager] create conversation instance (id: ${id})`);
|
||||
const _this = this;
|
||||
return new Conversation(id, function (
|
||||
idx: number,
|
||||
message: Message[],
|
||||
): boolean {
|
||||
return _this.callback(idx, message);
|
||||
});
|
||||
}
|
||||
|
||||
public async add(id: number): Promise<void> {
|
||||
if (this.conversations[id]) return;
|
||||
const instance = this.createConversation(id);
|
||||
this.conversations[id] = instance;
|
||||
|
||||
const res = await loadConversation(id);
|
||||
instance.load(res.message);
|
||||
instance.setModel(res.model);
|
||||
}
|
||||
|
||||
public async load(id: number, data: Message[]): Promise<void> {
|
||||
if (!this.conversations[id]) await this.add(id);
|
||||
this.conversations[id].load(data);
|
||||
}
|
||||
|
||||
public async toggle(dispatch: AppDispatch, id: number): Promise<void> {
|
||||
if (!this.conversations[id]) await this.add(id);
|
||||
|
||||
this.current = id;
|
||||
dispatch(setCurrent(id));
|
||||
this.get(id)!.toggle(dispatch);
|
||||
}
|
||||
|
||||
public async delete(dispatch: AppDispatch, id: number): Promise<void> {
|
||||
if (this.getCurrent() === id) await this.toggle(dispatch, -1);
|
||||
dispatch(removeHistory(id));
|
||||
if (this.conversations[id]) delete this.conversations[id];
|
||||
}
|
||||
|
||||
public async deleteAll(dispatch: AppDispatch): Promise<void> {
|
||||
const ids = Object.keys(this.conversations).map((v) => parseInt(v));
|
||||
for (const id of ids) await this.delete(dispatch, id);
|
||||
|
||||
await this.toggle(dispatch, -1);
|
||||
}
|
||||
|
||||
public async reset(dispatch: AppDispatch): Promise<void> {
|
||||
await this.deleteAll(dispatch);
|
||||
await this.toggle(dispatch, -1);
|
||||
}
|
||||
|
||||
public async send(t: any, auth: boolean, props: ChatProps): Promise<boolean> {
|
||||
const id = this.getCurrent();
|
||||
if (!this.conversations[id]) return false;
|
||||
console.debug(
|
||||
`[chat] trigger send event: ${props.message} (type: ${
|
||||
auth ? "user" : "anonymous"
|
||||
}, id: ${id})`,
|
||||
);
|
||||
if (id === -1 && auth) {
|
||||
// check for raise conversation
|
||||
console.debug(
|
||||
`[manager] raise new conversation (name: ${props.message})`,
|
||||
);
|
||||
const { hook, useHook } = useShared<number>();
|
||||
this.dispatch?.(
|
||||
addHistory({
|
||||
message: props.message,
|
||||
hook,
|
||||
}),
|
||||
);
|
||||
const target = await useHook();
|
||||
this.conversations[target] = this.conversations[-1];
|
||||
delete this.conversations[-1]; // fix pointer
|
||||
this.conversations[-1] = this.createConversation(-1);
|
||||
this.current = target;
|
||||
return this.get(target)!.sendMessageWithRaise(t, target, props);
|
||||
}
|
||||
const conversation = this.get(id);
|
||||
if (!conversation) return false;
|
||||
return conversation.sendMessage(t, props);
|
||||
}
|
||||
|
||||
public get(id: number): Conversation | undefined {
|
||||
if (!this.conversations[id]) return undefined;
|
||||
return this.conversations[id];
|
||||
}
|
||||
}
|
||||
|
||||
export const manager = new Manager();
|
@ -1,5 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { Message } from "./types.ts";
|
||||
import { Message } from "./types.tsx";
|
||||
|
||||
export type SharingForm = {
|
||||
status: boolean;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Conversation } from "./conversation.ts";
|
||||
import { ChargeBaseProps } from "@/admin/charge.ts";
|
||||
import { useMemo } from "react";
|
||||
import { BotIcon, ServerIcon, UserIcon } from "lucide-react";
|
||||
|
||||
export const UserRole = "user";
|
||||
export const AssistantRole = "assistant";
|
||||
@ -7,6 +8,21 @@ export const SystemRole = "system";
|
||||
export type Role = typeof UserRole | typeof AssistantRole | typeof SystemRole;
|
||||
export const Roles = [UserRole, AssistantRole, SystemRole];
|
||||
|
||||
export const getRoleIcon = (role: string) => {
|
||||
return useMemo(() => {
|
||||
switch (role) {
|
||||
case UserRole:
|
||||
return <UserIcon />;
|
||||
case AssistantRole:
|
||||
return <BotIcon />;
|
||||
case SystemRole:
|
||||
return <ServerIcon />;
|
||||
default:
|
||||
return <UserIcon />;
|
||||
}
|
||||
}, [role]);
|
||||
};
|
||||
|
||||
export type Message = {
|
||||
role: string;
|
||||
content: string;
|
||||
@ -40,8 +56,6 @@ export type ConversationInstance = {
|
||||
shared?: boolean;
|
||||
};
|
||||
|
||||
export type ConversationMapper = Record<Id, Conversation>;
|
||||
|
||||
export type PlanItem = {
|
||||
id: string;
|
||||
name: string;
|
@ -1,5 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { Model, Plan } from "@/api/types.ts";
|
||||
import { Model, Plan } from "@/api/types.tsx";
|
||||
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
|
||||
import { getErrorMessage } from "@/utils/base.ts";
|
||||
|
||||
|
@ -139,6 +139,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar-wrapper {
|
||||
.message-avatar {
|
||||
display: flex;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: var(--radius);
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid hsl(var(--border));
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
&.user {
|
||||
align-items: flex-end;
|
||||
}
|
||||
@ -154,8 +172,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.assistant,
|
||||
&.system {
|
||||
&.assistant, &.system {
|
||||
align-items: flex-start;
|
||||
|
||||
.message-content {
|
||||
@ -167,6 +184,11 @@
|
||||
border-color: var(--assistant-border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar-wrapper {
|
||||
margin-right: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Message } from "@/api/types.ts";
|
||||
import { getRoleIcon, Message } from "@/api/types.tsx";
|
||||
import Markdown from "@/components/Markdown.tsx";
|
||||
import {
|
||||
CalendarCheck2,
|
||||
@ -28,6 +28,11 @@ import {
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
import EditorProvider from "@/components/EditorProvider.tsx";
|
||||
import Avatar from "@/components/Avatar.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectUsername } from "@/store/auth.ts";
|
||||
import { appLogo } from "@/conf/env.ts";
|
||||
import Icon from "@/components/utils/Icon.tsx";
|
||||
|
||||
type MessageProps = {
|
||||
index: number;
|
||||
@ -92,15 +97,20 @@ function MessageQuota({ message }: MessageQuotaProps) {
|
||||
function MessageContent({ message, end, index, onEvent }: MessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const isAssistant = message.role === "assistant";
|
||||
const isUser = message.role === "user";
|
||||
|
||||
const username = useSelector(selectUsername);
|
||||
const icon = getRoleIcon(message.role);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dropdown, setDropdown] = useState(false);
|
||||
const [editedMessage, setEditedMessage] = useState<string | undefined>("");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"content-wrapper",
|
||||
isAssistant ? "flex-row" : "flex-row-reverse",
|
||||
!isUser ? "flex-row" : "flex-row-reverse",
|
||||
)}
|
||||
>
|
||||
<EditorProvider
|
||||
@ -111,6 +121,21 @@ function MessageContent({ message, end, index, onEvent }: MessageProps) {
|
||||
value={editedMessage ?? ""}
|
||||
onChange={setEditedMessage}
|
||||
/>
|
||||
<div className={`message-avatar-wrapper`}>
|
||||
<Tips
|
||||
classNamePopup={`flex flex-row items-center`}
|
||||
trigger={
|
||||
isUser ? (
|
||||
<Avatar className={`message-avatar`} username={username} />
|
||||
) : (
|
||||
<img src={appLogo} alt={``} className={`message-avatar p-1`} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon icon={icon} className={`h-4 w-4 mr-1`} />
|
||||
{message.role}
|
||||
</Tips>
|
||||
</div>
|
||||
<div className={`message-content`}>
|
||||
{message.content.length ? (
|
||||
<Markdown children={message.content} />
|
||||
@ -121,16 +146,18 @@ function MessageContent({ message, end, index, onEvent }: MessageProps) {
|
||||
)}
|
||||
</div>
|
||||
<div className={`message-toolbar`}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={dropdown} onOpenChange={setDropdown}>
|
||||
<DropdownMenuTrigger className={`outline-none`}>
|
||||
<MoreVertical className={`h-4 w-4 m-0.5`} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={`end`}>
|
||||
{isAssistant && end && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onEvent && onEvent(message.end !== false ? "restart" : "stop")
|
||||
}
|
||||
onClick={() => {
|
||||
onEvent &&
|
||||
onEvent(message.end !== false ? "restart" : "stop");
|
||||
setDropdown(false);
|
||||
}}
|
||||
>
|
||||
{message.end !== false ? (
|
||||
<>
|
||||
|
@ -1,19 +1,18 @@
|
||||
import { Button } from "./ui/button.tsx";
|
||||
import { selectMessages } from "@/store/chat.ts";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useConversationActions, useMessages } from "@/store/chat.ts";
|
||||
import { MessageSquarePlus } from "lucide-react";
|
||||
import { toggleConversation } from "@/api/history.ts";
|
||||
import Github from "@/components/ui/icons/Github.tsx";
|
||||
|
||||
function ProjectLink() {
|
||||
const dispatch = useDispatch();
|
||||
const messages = useSelector(selectMessages);
|
||||
const messages = useMessages();
|
||||
|
||||
const { toggle } = useConversationActions();
|
||||
|
||||
return messages.length > 0 ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={async () => await toggleConversation(dispatch, -1)}
|
||||
onClick={async () => await toggle(-1)}
|
||||
>
|
||||
<MessageSquarePlus className={`h-4 w-4`} />
|
||||
</Button>
|
||||
|
@ -5,7 +5,12 @@ import Broadcast from "@/components/Broadcast.tsx";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { bindMarket, getApiPlans } from "@/api/v1.ts";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { updateMasks, updateSupportModels } from "@/store/chat.ts";
|
||||
import {
|
||||
stack,
|
||||
updateMasks,
|
||||
updateSupportModels,
|
||||
useMessageActions,
|
||||
} from "@/store/chat.ts";
|
||||
import { dispatchSubscriptionData, setTheme } from "@/store/globals.ts";
|
||||
import { infoEvent } from "@/events/info.ts";
|
||||
import { setForm } from "@/store/info.ts";
|
||||
@ -14,10 +19,15 @@ import { useEffect } from "react";
|
||||
|
||||
function AppProvider() {
|
||||
const dispatch = useDispatch();
|
||||
const { receive } = useMessageActions();
|
||||
|
||||
useEffect(() => {
|
||||
infoEvent.bind((data) => dispatch(setForm(data)));
|
||||
themeEvent.bind((theme) => dispatch(setTheme(theme)));
|
||||
|
||||
stack.setCallback(async (id, message) => {
|
||||
await receive(id, message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffectAsync(async () => {
|
||||
|
@ -1,9 +1,12 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Message } from "@/api/types.ts";
|
||||
import { Message } from "@/api/types.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectCurrent, selectMessages } from "@/store/chat.ts";
|
||||
import {
|
||||
listenMessageEvent,
|
||||
selectCurrent,
|
||||
useMessages,
|
||||
} from "@/store/chat.ts";
|
||||
import MessageSegment from "@/components/Message.tsx";
|
||||
import { connectionEvent } from "@/events/connection.ts";
|
||||
import { chatEvent } from "@/events/chat.ts";
|
||||
import { addEventListeners } from "@/utils/dom.ts";
|
||||
|
||||
@ -13,7 +16,8 @@ type ChatInterfaceProps = {
|
||||
|
||||
function ChatInterface({ setTarget }: ChatInterfaceProps) {
|
||||
const ref = React.useRef(null);
|
||||
const messages: Message[] = useSelector(selectMessages);
|
||||
const messages: Message[] = useMessages();
|
||||
const process = listenMessageEvent();
|
||||
const current: number = useSelector(selectCurrent);
|
||||
const [scrollable, setScrollable] = React.useState(true);
|
||||
const [position, setPosition] = React.useState(0);
|
||||
@ -57,13 +61,8 @@ function ChatInterface({ setTarget }: ChatInterfaceProps) {
|
||||
<MessageSegment
|
||||
message={message}
|
||||
end={i === messages.length - 1}
|
||||
onEvent={(e: string, index?: number, message?: string) => {
|
||||
connectionEvent.emit({
|
||||
id: current,
|
||||
event: e,
|
||||
index,
|
||||
message,
|
||||
});
|
||||
onEvent={(event: string, index?: number, message?: string) => {
|
||||
process({ id: current, event, index, message });
|
||||
}}
|
||||
key={i}
|
||||
index={i}
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import FileAction from "@/components/FileProvider.tsx";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectAuthenticated, selectInit } from "@/store/auth.ts";
|
||||
import {
|
||||
listenMessageEvent,
|
||||
selectCurrent,
|
||||
selectMessages,
|
||||
selectModel,
|
||||
selectSupportModels,
|
||||
selectWeb,
|
||||
useMessageActions,
|
||||
useMessages,
|
||||
useWorking,
|
||||
} from "@/store/chat.ts";
|
||||
import { manager } from "@/api/manager.ts";
|
||||
import { formatMessage } from "@/utils/processor.ts";
|
||||
import ChatInterface from "@/components/home/ChatInterface.tsx";
|
||||
import EditorAction from "@/components/EditorProvider.tsx";
|
||||
@ -19,18 +20,7 @@ import { clearHistoryState, getQueryParam } from "@/utils/path.ts";
|
||||
import { forgetMemory, popMemory } from "@/utils/memory.ts";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { ToastAction } from "@/components/ui/toast.tsx";
|
||||
import {
|
||||
alignSelector,
|
||||
contextSelector,
|
||||
frequencyPenaltySelector,
|
||||
historySelector,
|
||||
maxTokensSelector,
|
||||
presencePenaltySelector,
|
||||
repetitionPenaltySelector,
|
||||
temperatureSelector,
|
||||
topKSelector,
|
||||
topPSelector,
|
||||
} from "@/store/settings.ts";
|
||||
import { alignSelector } from "@/store/settings.ts";
|
||||
import { FileArray } from "@/api/file.ts";
|
||||
import {
|
||||
MarketAction,
|
||||
@ -42,57 +32,36 @@ import ChatSpace from "@/components/home/ChatSpace.tsx";
|
||||
import ActionButton from "@/components/home/assemblies/ActionButton.tsx";
|
||||
import ChatInput from "@/components/home/assemblies/ChatInput.tsx";
|
||||
import ScrollAction from "@/components/home/assemblies/ScrollAction.tsx";
|
||||
import { connectionEvent } from "@/events/connection.ts";
|
||||
import { chatEvent } from "@/events/chat.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import { goAuth } from "@/utils/app.ts";
|
||||
import { getModelFromId } from "@/conf/model.ts";
|
||||
import { posterEvent } from "@/events/poster.ts";
|
||||
|
||||
type InterfaceProps = {
|
||||
setWorking: (working: boolean) => void;
|
||||
setTarget: (instance: HTMLElement | null) => void;
|
||||
};
|
||||
|
||||
function Interface(props: InterfaceProps) {
|
||||
const messages = useSelector(selectMessages);
|
||||
|
||||
useEffect(() => {
|
||||
const end =
|
||||
messages.length > 0 && (messages[messages.length - 1].end ?? true);
|
||||
const working = messages.length > 0 && !end;
|
||||
props.setWorking?.(working);
|
||||
}, [messages]);
|
||||
|
||||
const messages = useMessages();
|
||||
return messages.length > 0 ? <ChatInterface {...props} /> : <ChatSpace />;
|
||||
}
|
||||
|
||||
function ChatWrapper() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { send: sendAction } = useMessageActions();
|
||||
const process = listenMessageEvent();
|
||||
const [files, setFiles] = useState<FileArray>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [working, setWorking] = useState(false);
|
||||
const [visible, setVisibility] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const init = useSelector(selectInit);
|
||||
const current = useSelector(selectCurrent);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
const model = useSelector(selectModel);
|
||||
const web = useSelector(selectWeb);
|
||||
const history = useSelector(historySelector);
|
||||
const target = useRef(null);
|
||||
const context = useSelector(contextSelector);
|
||||
const align = useSelector(alignSelector);
|
||||
|
||||
const max_tokens = useSelector(maxTokensSelector);
|
||||
const temperature = useSelector(temperatureSelector);
|
||||
const top_p = useSelector(topPSelector);
|
||||
const top_k = useSelector(topKSelector);
|
||||
const presence_penalty = useSelector(presencePenaltySelector);
|
||||
const frequency_penalty = useSelector(frequencyPenaltySelector);
|
||||
const repetition_penalty = useSelector(repetitionPenaltySelector);
|
||||
|
||||
const working = useWorking();
|
||||
const supportModels = useSelector(selectSupportModels);
|
||||
|
||||
const requireAuth = useMemo(
|
||||
@ -102,9 +71,6 @@ function ChatWrapper() {
|
||||
|
||||
const [instance, setInstance] = useState<HTMLElement | null>(null);
|
||||
|
||||
manager.setDispatch(dispatch);
|
||||
chatEvent.addEventListener(() => setWorking(false));
|
||||
|
||||
function clearFile() {
|
||||
setFiles([]);
|
||||
}
|
||||
@ -127,23 +93,7 @@ function ChatWrapper() {
|
||||
|
||||
const message: string = formatMessage(files, data);
|
||||
if (message.length > 0 && data.trim().length > 0) {
|
||||
if (
|
||||
await manager.send(t, auth, {
|
||||
type: "chat",
|
||||
message,
|
||||
web,
|
||||
model,
|
||||
context: history,
|
||||
ignore_context: !context,
|
||||
max_tokens,
|
||||
temperature,
|
||||
top_p,
|
||||
top_k,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
repetition_penalty,
|
||||
})
|
||||
) {
|
||||
if (await sendAction(message)) {
|
||||
forgetMemory("history");
|
||||
clearFile();
|
||||
return true;
|
||||
@ -160,10 +110,7 @@ function ChatWrapper() {
|
||||
}
|
||||
|
||||
async function handleCancel() {
|
||||
connectionEvent.emit({
|
||||
id: current,
|
||||
event: "stop",
|
||||
});
|
||||
process({ id: current, event: "stop" });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -208,7 +155,7 @@ function ChatWrapper() {
|
||||
return (
|
||||
<div className={`chat-container`}>
|
||||
<div className={`chat-wrapper`}>
|
||||
<Interface setTarget={setInstance} setWorking={setWorking} />
|
||||
<Interface setTarget={setInstance} />
|
||||
<div className={`chat-input`}>
|
||||
<div className={`input-action`}>
|
||||
<ScrollAction
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { toggleConversation } from "@/api/history.ts";
|
||||
import { mobile } from "@/utils/device.ts";
|
||||
import { filterMessage } from "@/utils/processor.ts";
|
||||
import { setMenu } from "@/store/menu.ts";
|
||||
@ -17,9 +16,9 @@ import {
|
||||
} from "@/components/ui/dropdown-menu.tsx";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConversationInstance } from "@/api/types.ts";
|
||||
import { ConversationInstance } from "@/api/types.tsx";
|
||||
import { useState } from "react";
|
||||
import { closeMarket } from "@/store/chat.ts";
|
||||
import { closeMarket, useConversationActions } from "@/store/chat.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
|
||||
type ConversationSegmentProps = {
|
||||
@ -36,6 +35,7 @@ function ConversationSegment({
|
||||
operate,
|
||||
}: ConversationSegmentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { toggle } = useConversationActions();
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
@ -52,7 +52,7 @@ function ConversationSegment({
|
||||
target.parentElement?.classList.contains("delete")
|
||||
)
|
||||
return;
|
||||
await toggleConversation(dispatch, conversation.id);
|
||||
await toggle(conversation.id);
|
||||
if (mobile) dispatch(setMenu(false));
|
||||
dispatch(closeMarket());
|
||||
}}
|
||||
|
@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectAuthenticated } from "@/store/auth.ts";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { Model, Plans } from "@/api/types.ts";
|
||||
import { Model, Plans } from "@/api/types.tsx";
|
||||
import { modelEvent } from "@/events/model.ts";
|
||||
import { levelSelector } from "@/store/subscription.ts";
|
||||
import { teenagerSelector } from "@/store/package.ts";
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { isUrl, splitList } from "@/utils/base.ts";
|
||||
import { Model } from "@/api/types.ts";
|
||||
import { Model } from "@/api/types.tsx";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
addModelList,
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectAuthenticated, selectUsername } from "@/store/auth.ts";
|
||||
import { closeMarket, selectCurrent, selectHistory } from "@/store/chat.ts";
|
||||
import {
|
||||
closeMarket,
|
||||
selectCurrent,
|
||||
selectHistory,
|
||||
useConversationActions,
|
||||
} from "@/store/chat.ts";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ConversationInstance } from "@/api/types.ts";
|
||||
import { ConversationInstance } from "@/api/types.tsx";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { extractMessage, filterMessage } from "@/utils/processor.ts";
|
||||
import { copyClipboard } from "@/utils/dom.ts";
|
||||
import { useEffectAsync, useAnimation } from "@/utils/hook.ts";
|
||||
import { mobile } from "@/utils/device.ts";
|
||||
import {
|
||||
deleteAllConversations,
|
||||
deleteConversation,
|
||||
toggleConversation,
|
||||
updateConversationList,
|
||||
} from "@/api/history.ts";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { selectMenu, setMenu } from "@/store/menu.ts";
|
||||
import {
|
||||
@ -63,25 +62,29 @@ function SidebarAction({ setOperateConversation }: SidebarActionProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { toast } = useToast();
|
||||
const refresh = useRef(null);
|
||||
const {
|
||||
toggle,
|
||||
refresh: refreshAction,
|
||||
removeAll: removeAllAction,
|
||||
} = useConversationActions();
|
||||
const refreshRef = useRef(null);
|
||||
const [removeAll, setRemoveAll] = useState<boolean>(false);
|
||||
|
||||
async function handleDeleteAll(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (await deleteAllConversations(dispatch))
|
||||
toast({
|
||||
title: t("conversation.delete-success"),
|
||||
description: t("conversation.delete-success-prompt"),
|
||||
});
|
||||
else
|
||||
toast({
|
||||
title: t("conversation.delete-failed"),
|
||||
description: t("conversation.delete-failed-prompt"),
|
||||
});
|
||||
(await removeAllAction())
|
||||
? toast({
|
||||
title: t("conversation.delete-success"),
|
||||
description: t("conversation.delete-success-prompt"),
|
||||
})
|
||||
: toast({
|
||||
title: t("conversation.delete-failed"),
|
||||
description: t("conversation.delete-failed-prompt"),
|
||||
});
|
||||
|
||||
await updateConversationList(dispatch);
|
||||
await refreshAction();
|
||||
setOperateConversation({ target: null, type: "" });
|
||||
setRemoveAll(false);
|
||||
}
|
||||
@ -92,7 +95,7 @@ function SidebarAction({ setOperateConversation }: SidebarActionProps) {
|
||||
variant={`ghost`}
|
||||
size={`icon`}
|
||||
onClick={async () => {
|
||||
await toggleConversation(dispatch, -1);
|
||||
await toggle(-1);
|
||||
if (mobile) dispatch(setMenu(false));
|
||||
dispatch(closeMarket());
|
||||
}}
|
||||
@ -128,17 +131,10 @@ function SidebarAction({ setOperateConversation }: SidebarActionProps) {
|
||||
variant={`ghost`}
|
||||
size={`icon`}
|
||||
id={`refresh`}
|
||||
ref={refresh}
|
||||
ref={refreshRef}
|
||||
onClick={() => {
|
||||
const hook = useAnimation(refresh, "active", 500);
|
||||
updateConversationList(dispatch)
|
||||
.catch(() =>
|
||||
toast({
|
||||
title: t("conversation.refresh-failed"),
|
||||
description: t("conversation.refresh-failed-prompt"),
|
||||
}),
|
||||
)
|
||||
.finally(hook);
|
||||
const hook = useAnimation(refreshRef, "active", 500);
|
||||
refreshAction().finally(hook);
|
||||
}}
|
||||
>
|
||||
<RotateCw className={`h-4 w-4`} />
|
||||
@ -152,8 +148,8 @@ function SidebarConversationList({
|
||||
setOperateConversation,
|
||||
}: ConversationListProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { toast } = useToast();
|
||||
const { remove } = useConversationActions();
|
||||
const history: ConversationInstance[] = useSelector(selectHistory);
|
||||
const [shared, setShared] = useState<string>("");
|
||||
const current = useSelector(selectCurrent);
|
||||
@ -162,9 +158,7 @@ function SidebarConversationList({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (
|
||||
await deleteConversation(dispatch, operateConversation?.target?.id || -1)
|
||||
)
|
||||
if (await remove(operateConversation?.target?.id || -1))
|
||||
toast({
|
||||
title: t("conversation.delete-success"),
|
||||
description: t("conversation.delete-success-prompt"),
|
||||
@ -336,16 +330,14 @@ function SidebarMenu() {
|
||||
}
|
||||
function SideBar() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { refresh } = useConversationActions();
|
||||
const open = useSelector(selectMenu);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
const [operateConversation, setOperateConversation] = useState<Operation>({
|
||||
target: null,
|
||||
type: "",
|
||||
});
|
||||
useEffectAsync(async () => {
|
||||
await updateConversationList(dispatch);
|
||||
}, []);
|
||||
useEffectAsync(async () => await refresh(), []);
|
||||
|
||||
return (
|
||||
<div className={cn("sidebar", open && "open")}>
|
||||
|
@ -4,9 +4,8 @@ import { chatEvent } from "@/events/chat.ts";
|
||||
import { addEventListeners, scrollDown } from "@/utils/dom.ts";
|
||||
import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Message } from "@/api/types.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectMessages } from "@/store/chat.ts";
|
||||
import { Message } from "@/api/types.tsx";
|
||||
import { useMessages } from "@/store/chat.ts";
|
||||
|
||||
type ScrollActionProps = {
|
||||
visible: boolean;
|
||||
@ -16,7 +15,7 @@ type ScrollActionProps = {
|
||||
|
||||
function ScrollAction({ visible, target, setVisibility }: ScrollActionProps) {
|
||||
const { t } = useTranslation();
|
||||
const messages: Message[] = useSelector(selectMessages);
|
||||
const messages: Message[] = useMessages();
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) return setVisibility(false);
|
||||
|
@ -28,7 +28,7 @@ import { deeptrainEndpoint, useDeeptrain } from "@/conf/env.ts";
|
||||
import { AppDispatch } from "@/store";
|
||||
import { openDialog } from "@/store/quota.ts";
|
||||
import { getPlanPrice } from "@/conf/subscription.tsx";
|
||||
import { Plans } from "@/api/types.ts";
|
||||
import { Plans } from "@/api/types.tsx";
|
||||
import { subscriptionDataSelector } from "@/store/globals.ts";
|
||||
|
||||
function countPrice(data: Plans, base: number, month: number): number {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Model } from "@/api/types.ts";
|
||||
import { Model } from "@/api/types.tsx";
|
||||
|
||||
export function getModelFromId(market: Model[], id: string): Model | undefined {
|
||||
return market.find((model) => model.id === id);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getMemory, setMemory } from "@/utils/memory.ts";
|
||||
import { Model, Plan } from "@/api/types.ts";
|
||||
import { Model, Plan } from "@/api/types.tsx";
|
||||
|
||||
export function savePreferenceModels(models: Model[]): void {
|
||||
setMemory("model_preference", models.map((item) => item.id).join(","));
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
Flame,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Plan, Plans } from "@/api/types.ts";
|
||||
import { Plan, Plans } from "@/api/types.tsx";
|
||||
import Icon from "@/components/utils/Icon.tsx";
|
||||
|
||||
export const subscriptionIcons: Record<string, React.ReactElement> = {
|
||||
|
@ -14,16 +14,15 @@ import {
|
||||
selectMask,
|
||||
setMask,
|
||||
updateMasks,
|
||||
useConversationActions,
|
||||
} from "@/store/chat.ts";
|
||||
import { MASKS } from "@/masks/prompts.ts";
|
||||
import { CustomMask, initialCustomMask, Mask } from "@/masks/types.ts";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import React, { useMemo, useReducer, useState } from "react";
|
||||
import { splitList } from "@/utils/base.ts";
|
||||
import { maskEvent } from "@/events/mask.ts";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
FolderInput,
|
||||
@ -32,15 +31,13 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
Server,
|
||||
Trash,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import EmojiPicker, { Theme } from "emoji-picker-react";
|
||||
import { themeSelector } from "@/store/globals.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
import { AssistantRole, Roles, SystemRole, UserRole } from "@/api/types.ts";
|
||||
import { getRoleIcon, Roles, UserRole } from "@/api/types.tsx";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
@ -90,6 +87,8 @@ type RoleActionProps = {
|
||||
};
|
||||
|
||||
function RoleAction({ role, onClick }: RoleActionProps) {
|
||||
const icon = getRoleIcon(role);
|
||||
|
||||
const toggle = () => {
|
||||
const index = Roles.indexOf(role);
|
||||
const next = (index + 1) % Roles.length;
|
||||
@ -97,19 +96,6 @@ function RoleAction({ role, onClick }: RoleActionProps) {
|
||||
onClick(Roles[next]);
|
||||
};
|
||||
|
||||
const icon = useMemo(() => {
|
||||
switch (role) {
|
||||
case UserRole:
|
||||
return <User />;
|
||||
case AssistantRole:
|
||||
return <Bot />;
|
||||
case SystemRole:
|
||||
return <Server />;
|
||||
default:
|
||||
return <User />;
|
||||
}
|
||||
}, [role]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={`outline`}
|
||||
@ -133,6 +119,8 @@ function MaskItem({ mask, event, custom }: MaskItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mask: setMask } = useConversationActions();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const prevent = (e: React.MouseEvent) => {
|
||||
@ -146,7 +134,7 @@ function MaskItem({ mask, event, custom }: MaskItemProps) {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
maskEvent.emit(mask);
|
||||
setMask(mask);
|
||||
dispatch(closeMask());
|
||||
}}
|
||||
>
|
||||
@ -174,7 +162,7 @@ function MaskItem({ mask, event, custom }: MaskItemProps) {
|
||||
onClick={(e) => {
|
||||
prevent(e);
|
||||
|
||||
maskEvent.emit(mask);
|
||||
setMask(mask);
|
||||
dispatch(closeMask());
|
||||
|
||||
setOpen(false);
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { EventCommitter } from "./struct.ts";
|
||||
|
||||
export type ConnectionEvent = {
|
||||
id: number;
|
||||
event: string;
|
||||
index?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export const connectionEvent = new EventCommitter<ConnectionEvent>({
|
||||
name: "connection",
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
import { Mask } from "@/masks/types.ts";
|
||||
import { EventCommitter } from "@/events/struct.ts";
|
||||
|
||||
export const maskEvent = new EventCommitter<Mask>({
|
||||
name: "mask",
|
||||
destroyedAfterTrigger: true,
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { EventCommitter } from "./struct.ts";
|
||||
import { Message } from "@/api/types.ts";
|
||||
import { Message } from "@/api/types.tsx";
|
||||
|
||||
export type SharingEvent = {
|
||||
refer: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserRole } from "@/api/types.ts";
|
||||
import { UserRole } from "@/api/types.tsx";
|
||||
|
||||
export type MaskMessage = {
|
||||
role: string;
|
||||
|
@ -20,7 +20,7 @@ import { Button } from "@/components/ui/button.tsx";
|
||||
import router from "@/router.tsx";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { sharingEvent } from "@/events/sharing.ts";
|
||||
import { Message } from "@/api/types.ts";
|
||||
import { Message } from "@/api/types.tsx";
|
||||
import Avatar from "@/components/Avatar.tsx";
|
||||
import { toJpeg } from "html-to-image";
|
||||
import { appLogo } from "@/conf/env.ts";
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
} from "@/components/ui/card.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dispatch, useMemo, useReducer, useState } from "react";
|
||||
import { Model as RawModel } from "@/api/types.ts";
|
||||
import { Model as RawModel } from "@/api/types.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import {
|
||||
Activity,
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
SubscriptionIcon,
|
||||
subscriptionIconsList,
|
||||
} from "@/conf/subscription.tsx";
|
||||
import { Plan, PlanItem } from "@/api/types.ts";
|
||||
import { Plan, PlanItem } from "@/api/types.tsx";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { ConversationInstance, Model } from "@/api/types.ts";
|
||||
import { Message } from "@/api/types.ts";
|
||||
import { insertStart } from "@/utils/base.ts";
|
||||
import {
|
||||
AssistantRole,
|
||||
ConversationInstance,
|
||||
Model,
|
||||
UserRole,
|
||||
} from "@/api/types.tsx";
|
||||
import { Message } from "@/api/types.tsx";
|
||||
import { AppDispatch, RootState } from "./index.ts";
|
||||
import {
|
||||
getArrayMemory,
|
||||
@ -15,16 +19,44 @@ import {
|
||||
loadPreferenceModels,
|
||||
setOfflineModels,
|
||||
} from "@/conf/storage.ts";
|
||||
import { CustomMask } from "@/masks/types.ts";
|
||||
import {
|
||||
deleteConversation as doDeleteConversation,
|
||||
deleteAllConversations as doDeleteAllConversations,
|
||||
loadConversation,
|
||||
getConversationList,
|
||||
} from "@/api/history.ts";
|
||||
import { CustomMask, Mask } from "@/masks/types.ts";
|
||||
import { listMasks } from "@/api/mask.ts";
|
||||
import { ConversationSerialized } from "@/api/conversation.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useMemo } from "react";
|
||||
import { ConnectionStack } from "@/api/connection.ts";
|
||||
import { ConnectionStack, StreamMessage } from "@/api/connection.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
contextSelector,
|
||||
frequencyPenaltySelector,
|
||||
historySelector,
|
||||
maxTokensSelector,
|
||||
presencePenaltySelector,
|
||||
repetitionPenaltySelector,
|
||||
temperatureSelector,
|
||||
topKSelector,
|
||||
topPSelector,
|
||||
} from "@/store/settings.ts";
|
||||
|
||||
export type ConversationSerialized = {
|
||||
model?: string;
|
||||
messages: Message[];
|
||||
};
|
||||
|
||||
export type ConnectionEvent = {
|
||||
id: number;
|
||||
event: string;
|
||||
index?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type initialStateType = {
|
||||
history: ConversationInstance[];
|
||||
messages: Message[];
|
||||
conversations: Record<number, ConversationSerialized>;
|
||||
model: string;
|
||||
web: boolean;
|
||||
@ -32,10 +64,13 @@ type initialStateType = {
|
||||
model_list: string[];
|
||||
market: boolean;
|
||||
mask: boolean;
|
||||
mask_item: Mask | null;
|
||||
custom_masks: CustomMask[];
|
||||
support_models: Model[];
|
||||
};
|
||||
|
||||
const defaultConversation: ConversationSerialized = { messages: [] };
|
||||
|
||||
export function inModel(supportModels: Model[], model: string): boolean {
|
||||
return (
|
||||
model.length > 0 &&
|
||||
@ -72,7 +107,9 @@ const chatSlice = createSlice({
|
||||
initialState: {
|
||||
history: [],
|
||||
messages: [],
|
||||
conversations: {},
|
||||
conversations: {
|
||||
[-1]: { ...defaultConversation },
|
||||
},
|
||||
web: getBooleanMemory("web", false),
|
||||
current: -1,
|
||||
model: getModel(offline, getMemory("model")),
|
||||
@ -83,30 +120,142 @@ const chatSlice = createSlice({
|
||||
),
|
||||
market: false,
|
||||
mask: false,
|
||||
mask_item: null,
|
||||
custom_masks: [],
|
||||
support_models: offline,
|
||||
} as initialStateType,
|
||||
reducers: {
|
||||
createMessage: (state, action) => {
|
||||
const { id, role, content } = action.payload as {
|
||||
id: number;
|
||||
role: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
const conversation = state.conversations[id];
|
||||
if (!conversation) return;
|
||||
|
||||
if (id === -1 && state.mask_item && conversation.messages.length === 0) {
|
||||
conversation.messages = [...state.mask_item.context];
|
||||
state.mask_item = null;
|
||||
}
|
||||
|
||||
conversation.messages.push({
|
||||
role: role ?? AssistantRole,
|
||||
content: content ?? "",
|
||||
end: role === AssistantRole ? false : undefined,
|
||||
});
|
||||
},
|
||||
updateMessage: (state, action) => {
|
||||
const { id, message } = action.payload as {
|
||||
id: number;
|
||||
message: StreamMessage;
|
||||
};
|
||||
const conversation = state.conversations[id];
|
||||
if (!conversation) return;
|
||||
|
||||
if (conversation.messages.length === 0)
|
||||
conversation.messages.push({
|
||||
role: AssistantRole,
|
||||
content: message.message,
|
||||
keyword: message.keyword,
|
||||
quota: message.quota,
|
||||
end: message.end,
|
||||
plan: message.plan,
|
||||
});
|
||||
|
||||
const instance = conversation.messages[conversation.messages.length - 1];
|
||||
instance.content += message.message;
|
||||
if (message.keyword) instance.keyword = message.keyword;
|
||||
if (message.quota) instance.quota = message.quota;
|
||||
if (message.end) instance.end = message.end;
|
||||
instance.plan = message.plan;
|
||||
},
|
||||
removeMessage: (state, action) => {
|
||||
const { id, idx } = action.payload as { id: number; idx: number };
|
||||
const conversation = state.conversations[id];
|
||||
if (!conversation) return;
|
||||
|
||||
conversation.messages.splice(idx, 1);
|
||||
},
|
||||
restartMessage: (state, action) => {
|
||||
const id = action.payload as number;
|
||||
const conversation = state.conversations[id];
|
||||
if (!conversation || conversation.messages.length === 0) return;
|
||||
|
||||
const last = conversation.messages[conversation.messages.length - 1];
|
||||
if (last.role !== AssistantRole) return;
|
||||
conversation.messages.pop();
|
||||
|
||||
conversation.messages.push({
|
||||
role: AssistantRole,
|
||||
content: "",
|
||||
end: false,
|
||||
});
|
||||
},
|
||||
editMessage: (state, action) => {
|
||||
const { id, idx, message } = action.payload as {
|
||||
id: number;
|
||||
idx: number;
|
||||
message: string;
|
||||
};
|
||||
const conversation = state.conversations[id];
|
||||
if (!conversation || conversation.messages.length <= idx) return;
|
||||
|
||||
conversation.messages[idx].content = message;
|
||||
},
|
||||
stopMessage: (state, action) => {
|
||||
const { id } = action.payload as { id: number };
|
||||
const conversation = state.conversations[id];
|
||||
if (!conversation || conversation.messages.length === 0) return;
|
||||
|
||||
conversation.messages[conversation.messages.length - 1].end = true;
|
||||
},
|
||||
raiseConversation: (state, action) => {
|
||||
// raise conversation `-1` to target id
|
||||
const id = action.payload as number;
|
||||
const conversation = state.conversations[-1];
|
||||
if (!conversation || id === -1) return;
|
||||
|
||||
state.conversations[id] = conversation;
|
||||
if (state.current === -1) state.current = id;
|
||||
|
||||
state.conversations[-1] = { ...defaultConversation };
|
||||
},
|
||||
importConversation: (state, action) => {
|
||||
const { conversation, id } = action.payload as {
|
||||
conversation: ConversationSerialized;
|
||||
id: number;
|
||||
};
|
||||
|
||||
if (state.conversations[id]) return;
|
||||
state.conversations[id] = conversation;
|
||||
},
|
||||
deleteConversation: (state, action) => {
|
||||
const id = action.payload as number;
|
||||
|
||||
if (id === -1) return;
|
||||
if (!state.conversations[id]) return;
|
||||
|
||||
if (state.current === id) state.current = -1;
|
||||
delete state.conversations[id];
|
||||
|
||||
state.history = state.history.filter((item) => item.id !== id);
|
||||
},
|
||||
deleteAllConversation: (state) => {
|
||||
state.history = [];
|
||||
|
||||
state.conversations = { [-1]: { ...defaultConversation } };
|
||||
state.current = -1;
|
||||
},
|
||||
setHistory: (state, action) => {
|
||||
state.history = action.payload as ConversationInstance[];
|
||||
},
|
||||
removeHistory: (state, action) => {
|
||||
state.history = state.history.filter(
|
||||
(item) => item.id !== (action.payload as number),
|
||||
);
|
||||
},
|
||||
addHistory: (state, action) => {
|
||||
const name = action.payload.message as string;
|
||||
const id = state.history.length
|
||||
? Math.max(...state.history.map((item) => item.id)) + 1
|
||||
: 1;
|
||||
preflightHistory: (state, action) => {
|
||||
const name = action.payload as string;
|
||||
|
||||
state.history = insertStart(state.history, { id, name, message: [] });
|
||||
state.current = id;
|
||||
action.payload.hook(id);
|
||||
},
|
||||
setMessages: (state, action) => {
|
||||
state.messages = action.payload as Message[];
|
||||
// add a new history at the beginning
|
||||
state.history = [{ id: -1, name, message: [] }, ...state.history];
|
||||
},
|
||||
setModel: (state, action) => {
|
||||
setMemory("model", action.payload as string);
|
||||
@ -122,13 +271,17 @@ const chatSlice = createSlice({
|
||||
state.web = web;
|
||||
},
|
||||
setCurrent: (state, action) => {
|
||||
state.current = action.payload as number;
|
||||
},
|
||||
addMessage: (state, action) => {
|
||||
state.messages.push(action.payload as Message);
|
||||
},
|
||||
setMessage: (state, action) => {
|
||||
state.messages[state.messages.length - 1] = action.payload as Message;
|
||||
const current = action.payload as number;
|
||||
state.current = current;
|
||||
|
||||
const conversation = state.conversations[current];
|
||||
if (!conversation) return;
|
||||
if (
|
||||
conversation.model &&
|
||||
inModel(state.support_models, conversation.model)
|
||||
) {
|
||||
state.model = conversation.model;
|
||||
}
|
||||
},
|
||||
setModelList: (state, action) => {
|
||||
const models = action.payload as string[];
|
||||
@ -175,6 +328,12 @@ const chatSlice = createSlice({
|
||||
closeMask: (state) => {
|
||||
state.mask = false;
|
||||
},
|
||||
setMaskItem: (state, action) => {
|
||||
state.mask_item = action.payload as Mask;
|
||||
},
|
||||
clearMaskItem: (state) => {
|
||||
state.mask_item = null;
|
||||
},
|
||||
setCustomMasks: (state, action) => {
|
||||
state.custom_masks = action.payload as CustomMask[];
|
||||
},
|
||||
@ -196,15 +355,10 @@ const chatSlice = createSlice({
|
||||
|
||||
export const {
|
||||
setHistory,
|
||||
removeHistory,
|
||||
addHistory,
|
||||
setCurrent,
|
||||
setMessages,
|
||||
setModel,
|
||||
setWeb,
|
||||
toggleWeb,
|
||||
addMessage,
|
||||
setMessage,
|
||||
setModelList,
|
||||
addModelList,
|
||||
removeModelList,
|
||||
@ -216,11 +370,22 @@ export const {
|
||||
closeMask,
|
||||
setCustomMasks,
|
||||
setSupportModels,
|
||||
setMaskItem,
|
||||
clearMaskItem,
|
||||
createMessage,
|
||||
updateMessage,
|
||||
removeMessage,
|
||||
restartMessage,
|
||||
editMessage,
|
||||
stopMessage,
|
||||
raiseConversation,
|
||||
importConversation,
|
||||
deleteConversation,
|
||||
deleteAllConversation,
|
||||
preflightHistory,
|
||||
} = chatSlice.actions;
|
||||
export const selectHistory = (state: RootState): ConversationInstance[] =>
|
||||
state.chat.history;
|
||||
export const selectMessages = (state: RootState): Message[] =>
|
||||
state.chat.messages;
|
||||
export const selectConversations = (
|
||||
state: RootState,
|
||||
): Record<number, ConversationSerialized> => state.chat.conversations;
|
||||
@ -235,6 +400,8 @@ export const selectCustomMasks = (state: RootState): CustomMask[] =>
|
||||
state.chat.custom_masks;
|
||||
export const selectSupportModels = (state: RootState): Model[] =>
|
||||
state.chat.support_models;
|
||||
export const selectMaskItem = (state: RootState): Mask | null =>
|
||||
state.chat.mask_item;
|
||||
|
||||
export function useConversation(): ConversationSerialized | undefined {
|
||||
const conversations = useSelector(selectConversations);
|
||||
@ -243,14 +410,211 @@ export function useConversation(): ConversationSerialized | undefined {
|
||||
return useMemo(() => conversations[current], [conversations, current]);
|
||||
}
|
||||
|
||||
export function useConversationActions() {
|
||||
const dispatch = useDispatch();
|
||||
const conversations = useSelector(selectConversations);
|
||||
const current = useSelector(selectCurrent);
|
||||
const mask = useSelector(selectMaskItem);
|
||||
|
||||
return {
|
||||
toggle: async (id: number) => {
|
||||
const conversation = conversations[id];
|
||||
if (!conversation) {
|
||||
const data = await loadConversation(id);
|
||||
const props: ConversationSerialized = {
|
||||
model: data.model,
|
||||
messages: data.message,
|
||||
};
|
||||
|
||||
dispatch(
|
||||
importConversation({
|
||||
conversation: props,
|
||||
id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (current === -1 && conversations[-1].messages.length === 0) {
|
||||
// current is mask, clear mask
|
||||
mask && dispatch(clearMaskItem());
|
||||
}
|
||||
|
||||
dispatch(setCurrent(id));
|
||||
},
|
||||
remove: async (id: number) => {
|
||||
const state = await doDeleteConversation(id);
|
||||
state && dispatch(deleteConversation(id));
|
||||
|
||||
return state;
|
||||
},
|
||||
removeAll: async () => {
|
||||
const state = await doDeleteAllConversations();
|
||||
state && dispatch(deleteAllConversation());
|
||||
|
||||
return state;
|
||||
},
|
||||
refresh: async () => {
|
||||
const resp = await getConversationList();
|
||||
dispatch(setHistory(resp));
|
||||
},
|
||||
mask: (mask: Mask) => {
|
||||
dispatch(setMaskItem(mask));
|
||||
|
||||
if (current !== -1) {
|
||||
dispatch(setCurrent(-1));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useMessageActions() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { refresh } = useConversationActions();
|
||||
const current = useSelector(selectCurrent);
|
||||
const conversations = useSelector(selectConversations);
|
||||
const mask = useSelector(selectMaskItem);
|
||||
|
||||
const model = useSelector(selectModel);
|
||||
const web = useSelector(selectWeb);
|
||||
const history = useSelector(historySelector);
|
||||
const context = useSelector(contextSelector);
|
||||
const max_tokens = useSelector(maxTokensSelector);
|
||||
const temperature = useSelector(temperatureSelector);
|
||||
const top_p = useSelector(topPSelector);
|
||||
const top_k = useSelector(topKSelector);
|
||||
const presence_penalty = useSelector(presencePenaltySelector);
|
||||
const frequency_penalty = useSelector(frequencyPenaltySelector);
|
||||
const repetition_penalty = useSelector(repetitionPenaltySelector);
|
||||
|
||||
return {
|
||||
send: async (message: string) => {
|
||||
if (current === -1 && conversations[-1].messages.length === 0) {
|
||||
// preflight history if it's a new conversation
|
||||
dispatch(preflightHistory(message));
|
||||
}
|
||||
|
||||
if (!stack.hasConnection(current)) {
|
||||
const conn = stack.createConnection(current);
|
||||
|
||||
if (current === -1 && mask && mask.context.length > 0) {
|
||||
conn.sendMaskEvent(t, mask);
|
||||
dispatch(clearMaskItem());
|
||||
}
|
||||
}
|
||||
|
||||
const state = stack.send(current, t, {
|
||||
type: "chat",
|
||||
message,
|
||||
web,
|
||||
model,
|
||||
context: history,
|
||||
ignore_context: !context,
|
||||
max_tokens,
|
||||
temperature,
|
||||
top_p,
|
||||
top_k,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
repetition_penalty,
|
||||
});
|
||||
if (!state) return false;
|
||||
|
||||
dispatch(
|
||||
createMessage({ id: current, role: UserRole, content: message }),
|
||||
);
|
||||
dispatch(createMessage({ id: current, role: AssistantRole }));
|
||||
|
||||
return true;
|
||||
},
|
||||
stop: () => {
|
||||
if (!stack.hasConnection(current)) return;
|
||||
stack.sendStopEvent(current, t);
|
||||
dispatch(stopMessage(current));
|
||||
},
|
||||
restart: () => {
|
||||
if (!stack.hasConnection(current)) {
|
||||
stack.createConnection(current);
|
||||
}
|
||||
stack.sendRestartEvent(current, t);
|
||||
|
||||
// remove the last message if it's from assistant and create a new message
|
||||
dispatch(restartMessage(current));
|
||||
},
|
||||
remove: (idx: number) => {
|
||||
if (idx < 0 || idx >= conversations[current].messages.length) return;
|
||||
|
||||
dispatch(removeMessage({ id: current, idx }));
|
||||
|
||||
if (!stack.hasConnection(current)) stack.createConnection(current);
|
||||
stack.sendRemoveEvent(current, t, idx);
|
||||
},
|
||||
edit: (idx: number, message: string) => {
|
||||
if (idx < 0 || idx >= conversations[current].messages.length) return;
|
||||
|
||||
dispatch(editMessage({ id: current, idx, message }));
|
||||
if (!stack.hasConnection(current)) stack.createConnection(current);
|
||||
stack.sendEditEvent(current, t, idx, message);
|
||||
},
|
||||
receive: async (id: number, message: StreamMessage) => {
|
||||
dispatch(updateMessage({ id, message }));
|
||||
|
||||
// raise conversation if it is -1
|
||||
if (id === -1 && message.conversation) {
|
||||
const target: number = message.conversation;
|
||||
dispatch(raiseConversation(target));
|
||||
stack.raiseConnection(target);
|
||||
await refresh();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function listenMessageEvent() {
|
||||
const actions = useMessageActions();
|
||||
|
||||
return (e: ConnectionEvent) => {
|
||||
console.debug(`[conversation] receive event: ${e.event} (id: ${e.id})`);
|
||||
|
||||
switch (e.event) {
|
||||
case "stop":
|
||||
actions.stop();
|
||||
break;
|
||||
case "restart":
|
||||
actions.restart();
|
||||
break;
|
||||
case "remove":
|
||||
actions.remove(e.index ?? -1);
|
||||
break;
|
||||
case "edit":
|
||||
actions.edit(e.index ?? -1, e.message ?? "");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useMessages(): Message[] {
|
||||
const conversations = useSelector(selectConversations);
|
||||
const current = useSelector(selectCurrent);
|
||||
const mask = useSelector(selectMaskItem);
|
||||
|
||||
return useMemo(
|
||||
() => conversations[current]?.messages || [],
|
||||
[conversations, current],
|
||||
);
|
||||
return useMemo(() => {
|
||||
const messages = conversations[current]?.messages || [];
|
||||
const showMask = current === -1 && mask && messages.length === 0;
|
||||
return !showMask ? messages : mask?.context;
|
||||
}, [conversations, current, mask]);
|
||||
}
|
||||
|
||||
export function useWorking(): boolean {
|
||||
const messages = useMessages();
|
||||
|
||||
return useMemo(() => {
|
||||
if (messages.length === 0) return false;
|
||||
|
||||
const last = messages[messages.length - 1];
|
||||
if (last.role !== AssistantRole || last.end === undefined) return false;
|
||||
return !last.end;
|
||||
}, [messages]);
|
||||
}
|
||||
|
||||
export const updateMasks = async (dispatch: AppDispatch) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { Plans } from "@/api/types.ts";
|
||||
import { Plans } from "@/api/types.tsx";
|
||||
import { AppDispatch, RootState } from "@/store/index.ts";
|
||||
import { getOfflinePlans, setOfflinePlans } from "@/conf/storage.ts";
|
||||
import { getTheme, Theme } from "@/components/ThemeProvider.tsx";
|
||||
|
@ -28,6 +28,11 @@ func NewChatRequest(group string, props *adapter.ChatProps, hook globals.Hook) e
|
||||
}
|
||||
|
||||
globals.Info(fmt.Sprintf("[channel] channels are exhausted for model %s", props.Model))
|
||||
|
||||
if err == nil {
|
||||
err = fmt.Errorf("channels are exhausted for model %s", props.Model)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -53,7 +58,7 @@ func PreflightCache(cache *redis.Client, hash string, buffer *utils.Buffer, hook
|
||||
buffer.SetInputTokens(buf.CountInputToken())
|
||||
buffer.SetToolCalls(buf.GetToolCalls())
|
||||
buffer.SetFunctionCall(buf.GetFunctionCall())
|
||||
|
||||
|
||||
return idx, true, hook(&globals.Chunk{
|
||||
Content: data,
|
||||
FunctionCall: buf.GetFunctionCall(),
|
||||
|
@ -109,6 +109,10 @@ func (c *Conversation) IsEnableWeb() bool {
|
||||
}
|
||||
|
||||
func (c *Conversation) GetContextLength() int {
|
||||
if c.Context <= 0 {
|
||||
return defaultConversationContext
|
||||
}
|
||||
|
||||
return c.Context
|
||||
}
|
||||
|
||||
@ -376,6 +380,19 @@ func (c *Conversation) RemoveLatestMessage() globals.Message {
|
||||
return c.RemoveMessage(len(c.Message) - 1)
|
||||
}
|
||||
|
||||
func (c *Conversation) RemoveLatestMessageWithRole(role string) globals.Message {
|
||||
if len(c.Message) == 0 {
|
||||
return globals.Message{}
|
||||
}
|
||||
|
||||
message := c.Message[len(c.Message)-1]
|
||||
if message.Role == role {
|
||||
return c.RemoveLatestMessage()
|
||||
}
|
||||
|
||||
return globals.Message{}
|
||||
}
|
||||
|
||||
func (c *Conversation) EditMessage(index int, message string) {
|
||||
if index < 0 || index >= len(c.Message) {
|
||||
return
|
||||
|
@ -92,14 +92,7 @@ func ChatAPI(c *gin.Context) {
|
||||
case ShareType:
|
||||
instance.LoadSharing(db, form.Message)
|
||||
case RestartType:
|
||||
if message := instance.RemoveLatestMessage(); message.Role != globals.Assistant {
|
||||
conn.Send(globals.ChatSegmentResponse{
|
||||
Message: "Hello, How can I assist you?",
|
||||
End: true,
|
||||
})
|
||||
return fmt.Errorf("message type error")
|
||||
}
|
||||
|
||||
instance.RemoveLatestMessageWithRole(globals.Assistant)
|
||||
response := ChatHandler(buf, user, instance)
|
||||
instance.SaveResponse(db, response)
|
||||
case MaskType:
|
||||
|
Loading…
Reference in New Issue
Block a user