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