mirror of
https://github.com/coaidev/coai.git
synced 2025-05-28 09:20:18 +09:00
feat: update quota tooltip and article generation style mobile adapter, edit message and delete message alpha version (#51)
This commit is contained in:
parent
06ccf4e25c
commit
a3a9e5a525
@ -31,8 +31,6 @@ type ChatProps struct {
|
||||
RequestProps
|
||||
|
||||
Model string
|
||||
Plan bool
|
||||
Infinity bool
|
||||
Message []globals.Message
|
||||
Token int
|
||||
PresencePenalty *float32
|
||||
@ -56,7 +54,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(
|
||||
props.Token == 0,
|
||||
utils.Multi(props.Infinity || props.Plan, nil, utils.ToPtr(2500)),
|
||||
utils.ToPtr(2500),
|
||||
&props.Token,
|
||||
),
|
||||
PresencePenalty: props.PresencePenalty,
|
||||
@ -74,7 +72,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(
|
||||
props.Token == 0,
|
||||
utils.Multi(props.Infinity || props.Plan, nil, utils.ToPtr(2500)),
|
||||
utils.ToPtr(2500),
|
||||
&props.Token,
|
||||
),
|
||||
PresencePenalty: props.PresencePenalty,
|
||||
@ -136,7 +134,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
return dashscope.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&dashscope.ChatProps{
|
||||
Model: model,
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(props.Infinity || props.Plan, 2048, props.Token),
|
||||
Token: props.Token,
|
||||
Temperature: props.Temperature,
|
||||
TopP: props.TopP,
|
||||
TopK: props.TopK,
|
||||
@ -164,7 +162,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
return skylark.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&skylark.ChatProps{
|
||||
Model: model,
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(props.Token == 0, 4096, props.Token),
|
||||
Token: props.Token,
|
||||
TopP: props.TopP,
|
||||
TopK: props.TopK,
|
||||
Temperature: props.Temperature,
|
||||
@ -178,7 +176,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
return zhinao.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&zhinao.ChatProps{
|
||||
Model: model,
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(props.Infinity || props.Plan, nil, utils.ToPtr(2048)),
|
||||
Token: &props.Token,
|
||||
TopP: props.TopP,
|
||||
TopK: props.TopK,
|
||||
Temperature: props.Temperature,
|
||||
@ -193,13 +191,9 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
|
||||
case globals.OneAPIChannelType:
|
||||
return oneapi.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&oneapi.ChatProps{
|
||||
Model: model,
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(
|
||||
props.Token == 0,
|
||||
utils.Multi(props.Plan || props.Infinity, nil, utils.ToPtr(2500)),
|
||||
&props.Token,
|
||||
),
|
||||
Model: model,
|
||||
Message: props.Message,
|
||||
Token: &props.Token,
|
||||
PresencePenalty: props.PresencePenalty,
|
||||
FrequencyPenalty: props.FrequencyPenalty,
|
||||
Temperature: props.Temperature,
|
||||
|
@ -18,11 +18,9 @@ func CreateGeneration(group, model, prompt, path string, plan bool, hook func(bu
|
||||
buffer := utils.NewBuffer(model, message, channel.ChargeInstance.GetCharge(model))
|
||||
|
||||
err := channel.NewChatRequest(group, &adapter.ChatProps{
|
||||
Model: model,
|
||||
Message: message,
|
||||
Plan: plan,
|
||||
Infinity: true,
|
||||
Buffer: *buffer,
|
||||
Model: model,
|
||||
Message: message,
|
||||
Buffer: *buffer,
|
||||
}, func(data string) error {
|
||||
buffer.Write(data)
|
||||
hook(buffer, data)
|
||||
|
@ -61,6 +61,27 @@ export class Conversation {
|
||||
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)) {
|
||||
delete this.data[idx];
|
||||
this.sendRemoveEvent(idx);
|
||||
this.triggerCallback();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.debug(
|
||||
`[conversation] unknown event: ${ev.event} (from: ${ev.id})`,
|
||||
@ -82,6 +103,10 @@ export class Conversation {
|
||||
this.sendEvent("stop");
|
||||
}
|
||||
|
||||
public isValidIndex(idx: number): boolean {
|
||||
return idx >= 0 && idx < this.data.length;
|
||||
}
|
||||
|
||||
public sendRestartEvent() {
|
||||
this.sendEvent("restart");
|
||||
}
|
||||
@ -90,6 +115,24 @@ export class Conversation {
|
||||
this.sendEvent("mask", JSON.stringify(mask.context));
|
||||
}
|
||||
|
||||
public sendEditEvent(id: number, message: string) {
|
||||
this.sendEvent(
|
||||
"edit",
|
||||
JSON.stringify({
|
||||
message: `${id}:${message}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public sendRemoveEvent(id: number) {
|
||||
this.sendEvent(
|
||||
"remove",
|
||||
JSON.stringify({
|
||||
message: `${id}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public sendShareEvent(refer: string) {
|
||||
this.sendEvent("share", refer);
|
||||
}
|
||||
|
@ -30,6 +30,12 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.article-action {
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -63,7 +63,6 @@
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
|
||||
.message-toolbar {
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog.tsx";
|
||||
import { Maximize, Image, MenuSquare, PanelRight, XSquare } from "lucide-react";
|
||||
import { Maximize, Image, MenuSquare, PanelRight, Eraser } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "@/assets/common/editor.less";
|
||||
import { Textarea } from "./ui/textarea.tsx";
|
||||
@ -64,7 +64,7 @@ function RichEditor({ value, onChange, maxLength }: RichEditorProps) {
|
||||
className={`h-8 w-8 p-0`}
|
||||
onClick={() => value && onChange("")}
|
||||
>
|
||||
<XSquare className={`h-3.5 w-3.5`} />
|
||||
<Eraser className={`h-3.5 w-3.5`} />
|
||||
</Button>
|
||||
<div className={`grow`} />
|
||||
<Toggle
|
||||
|
@ -9,72 +9,84 @@ import {
|
||||
Loader2,
|
||||
MoreVertical,
|
||||
MousePointerSquare,
|
||||
PencilLine,
|
||||
Power,
|
||||
RotateCcw,
|
||||
Trash,
|
||||
} from "lucide-react";
|
||||
import { filterMessage } from "@/utils/processor.ts";
|
||||
import { copyClipboard, saveAsFile, useInputValue } from "@/utils/dom.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip.tsx";
|
||||
import { Ref, useRef } from "react";
|
||||
import { Ref, useMemo, useRef } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu.tsx";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
|
||||
type MessageProps = {
|
||||
index: number;
|
||||
message: Message;
|
||||
end?: boolean;
|
||||
onEvent?: (event: string) => void;
|
||||
onEvent?: (event: string, index?: number, message?: string) => void;
|
||||
ref?: Ref<HTMLElement>;
|
||||
};
|
||||
|
||||
function MessageSegment(props: MessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef(null);
|
||||
const { message } = props;
|
||||
|
||||
return (
|
||||
<div className={`message ${message.role}`} ref={ref}>
|
||||
<MessageContent {...props} />
|
||||
{message.quota && message.quota !== 0 ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`message-quota ${
|
||||
message.plan ? "subscription" : ""
|
||||
}`}
|
||||
>
|
||||
<Cloud className={`h-4 w-4 icon`} />
|
||||
<span className={`quota`}>
|
||||
{(message.quota < 0 ? 0 : message.quota).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={`icon-tooltip`}>
|
||||
<CloudFog className={`h-4 w-4 mr-2`} />
|
||||
<p>{t("quota-description")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : null}
|
||||
<MessageQuota message={message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageContent({ message, end, onEvent }: MessageProps) {
|
||||
const { t } = useTranslation();
|
||||
type MessageQuotaProps = {
|
||||
message: Message;
|
||||
};
|
||||
|
||||
function MessageQuota({ message }: MessageQuotaProps) {
|
||||
const trigger = useMemo(
|
||||
() =>
|
||||
message.quota && (
|
||||
<div className={cn("message-quota", message.plan && "subscription")}>
|
||||
<Cloud className={`h-4 w-4 icon`} />
|
||||
<span className={`quota`}>
|
||||
{(message.quota < 0 ? 0 : message.quota).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
[message],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`content-wrapper`}>
|
||||
message.quota &&
|
||||
message.quota !== 0 && (
|
||||
<Tips classNamePopup={`icon-tooltip justify-center`} trigger={trigger}>
|
||||
<CloudFog className={`h-4 w-4 mr-2`} />
|
||||
<p>{message.quota.toFixed(6)}</p>
|
||||
</Tips>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MessageContent({ message, end, index, onEvent }: MessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const isAssistant = message.role === "assistant";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"content-wrapper",
|
||||
isAssistant ? "flex-row" : "flex-row-reverse",
|
||||
)}
|
||||
>
|
||||
<div className={`message-content`}>
|
||||
{message.content.length ? (
|
||||
<Markdown children={message.content} />
|
||||
@ -84,62 +96,67 @@ function MessageContent({ message, end, onEvent }: MessageProps) {
|
||||
<Loader2 className={`h-5 w-5 m-1 animate-spin`} />
|
||||
)}
|
||||
</div>
|
||||
{message.role === "assistant" && (
|
||||
<div className={`message-toolbar`}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<MoreVertical className={`h-4 w-4 m-0.5`} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={`end`}>
|
||||
{end && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onEvent &&
|
||||
onEvent(message.end !== false ? "restart" : "stop")
|
||||
}
|
||||
>
|
||||
{message.end !== false ? (
|
||||
<>
|
||||
<RotateCcw className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.restart")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.stop")}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => copyClipboard(filterMessage(message.content))}
|
||||
>
|
||||
<Copy className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.copy")}
|
||||
</DropdownMenuItem>
|
||||
<div className={`message-toolbar`}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className={`outline-none`}>
|
||||
<MoreVertical className={`h-4 w-4 m-0.5`} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={`end`}>
|
||||
{isAssistant && end && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
useInputValue("input", filterMessage(message.content))
|
||||
onEvent && onEvent(message.end !== false ? "restart" : "stop")
|
||||
}
|
||||
>
|
||||
<MousePointerSquare className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.use")}
|
||||
{message.end !== false ? (
|
||||
<>
|
||||
<RotateCcw className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.restart")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.stop")}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
saveAsFile(
|
||||
`message-${message.role}.txt`,
|
||||
filterMessage(message.content),
|
||||
)
|
||||
}
|
||||
>
|
||||
<File className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.save")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => copyClipboard(filterMessage(message.content))}
|
||||
>
|
||||
<Copy className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.copy")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
useInputValue("input", filterMessage(message.content))
|
||||
}
|
||||
>
|
||||
<MousePointerSquare className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.use")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<PencilLine className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Trash className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.remove")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
saveAsFile(
|
||||
`message-${message.role}.txt`,
|
||||
filterMessage(message.content),
|
||||
)
|
||||
}
|
||||
>
|
||||
<File className={`h-4 w-4 mr-1.5`} />
|
||||
{t("message.save")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,12 +15,21 @@ import {
|
||||
|
||||
type TipsProps = {
|
||||
content?: string;
|
||||
trigger?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
classNamePopup?: string;
|
||||
hideTimeout?: number;
|
||||
};
|
||||
|
||||
function Tips({ content, children, className, hideTimeout }: TipsProps) {
|
||||
function Tips({
|
||||
content,
|
||||
trigger,
|
||||
children,
|
||||
className,
|
||||
classNamePopup,
|
||||
hideTimeout,
|
||||
}: TipsProps) {
|
||||
const timeout = hideTimeout ?? 2500;
|
||||
const comp = useMemo(
|
||||
() => (
|
||||
@ -49,14 +58,17 @@ function Tips({ content, children, className, hideTimeout }: TipsProps) {
|
||||
<TooltipProvider>
|
||||
<Tooltip open={tooltip} onOpenChange={setTooltip}>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className={cn("tips-icon", className)} />
|
||||
{trigger ?? <HelpCircle className={cn("tips-icon", className)} />}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{comp}</TooltipContent>
|
||||
<TooltipContent className={classNamePopup}>{comp}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className={"px-3 py-1.5 cursor-pointer text-sm"}
|
||||
className={cn(
|
||||
"px-3 py-1.5 cursor-pointer text-sm min-w-0",
|
||||
classNamePopup,
|
||||
)}
|
||||
side={`top`}
|
||||
>
|
||||
{comp}
|
||||
|
@ -64,13 +64,16 @@ function ChatInterface({ setTarget, setWorking }: ChatInterfaceProps) {
|
||||
<MessageSegment
|
||||
message={message}
|
||||
end={i === messages.length - 1}
|
||||
onEvent={(e: string) => {
|
||||
onEvent={(e: string, index?: number, message?: string) => {
|
||||
connectionEvent.emit({
|
||||
id: current,
|
||||
event: e,
|
||||
index,
|
||||
message,
|
||||
});
|
||||
}}
|
||||
key={i}
|
||||
index={i}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -21,9 +21,10 @@ function ChatInput({
|
||||
onEnterPressed,
|
||||
}: ChatInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [pressed, setPressed] = React.useState(false);
|
||||
const sender = useSelector(senderSelector);
|
||||
|
||||
// sender: Ctrl + Enter if false, Enter if true
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
id={`input`}
|
||||
@ -37,20 +38,16 @@ function ChatInput({
|
||||
}}
|
||||
placeholder={t("chat.placeholder")}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Control") {
|
||||
setPressed(true);
|
||||
} else if (e.key === "Enter" && !e.shiftKey) {
|
||||
if (sender || pressed) {
|
||||
if (e.key === "Enter") {
|
||||
if (sender || e.ctrlKey) {
|
||||
// condition sender: Ctrl + Enter if false, Enter if true
|
||||
// condition e.ctrlKey: Ctrl + Enter if true, Enter if false
|
||||
|
||||
e.preventDefault();
|
||||
onEnterPressed();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === "Control") {
|
||||
setTimeout(() => setPressed(false), 250);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import { EventCommitter } from "./struct.ts";
|
||||
export type ConnectionEvent = {
|
||||
id: number;
|
||||
event: string;
|
||||
index?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export const connectionEvent = new EventCommitter<ConnectionEvent>({
|
||||
|
@ -133,7 +133,9 @@
|
||||
"copy": "复制消息",
|
||||
"save": "保存为文件",
|
||||
"use": "使用消息",
|
||||
"edit": "编辑消息",
|
||||
"stop": "暂停回答",
|
||||
"remove": "删除消息",
|
||||
"restart": "重新回答",
|
||||
"copy-area": "复制选中区域"
|
||||
},
|
||||
|
@ -86,7 +86,9 @@
|
||||
"use": "Use Message",
|
||||
"stop": "Pause Answer",
|
||||
"restart": "Restart Answer",
|
||||
"copy-area": "Copy Selected Area"
|
||||
"copy-area": "Copy Selected Area",
|
||||
"edit": "Edit messages",
|
||||
"remove": "Delete a Message"
|
||||
},
|
||||
"quota-description": "spending quota for the message",
|
||||
"buy": {
|
||||
|
@ -86,7 +86,9 @@
|
||||
"use": "メッセージを使用する",
|
||||
"stop": "回答を一時停止",
|
||||
"restart": "再回答",
|
||||
"copy-area": "選択を複製"
|
||||
"copy-area": "選択を複製",
|
||||
"edit": "メッセージを編集",
|
||||
"remove": "メッセージを削除"
|
||||
},
|
||||
"quota-description": "メッセージのクレジット使用額",
|
||||
"buy": {
|
||||
|
@ -86,7 +86,9 @@
|
||||
"use": "Использовать сообщение",
|
||||
"stop": "Приостановить ответ",
|
||||
"restart": "Перезапустить ответ",
|
||||
"copy-area": "Копировать выбранную область"
|
||||
"copy-area": "Копировать выбранную область",
|
||||
"edit": "Редактировать сообщение",
|
||||
"remove": "Удалить сообщение"
|
||||
},
|
||||
"quota-description": "квота расходов на сообщение",
|
||||
"buy": {
|
||||
|
@ -33,16 +33,20 @@ function GenerateProgress({ current, total }: ProgressProps) {
|
||||
return (
|
||||
<div className={`article-progress w-full mb-4`}>
|
||||
<p
|
||||
className={`select-none mt-4 mb-2.5 flex flex-row items-center content-center mx-auto w-max`}
|
||||
className={`select-none mt-4 mb-2.5 flex flex-row items-center content-center w-full justify-center text-center`}
|
||||
>
|
||||
{total !== 0 && current === total ? (
|
||||
<>
|
||||
<Check className={`h-5 w-5 mr-2 inline-block animate-out`} />
|
||||
<Check
|
||||
className={`h-5 w-5 mr-2 inline-block animate-out shrink-0`}
|
||||
/>
|
||||
{t("article.generate-success")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className={`h-5 w-5 mr-2 inline-block animate-spin`} />
|
||||
<Loader2
|
||||
className={`h-5 w-5 mr-2 inline-block animate-spin shrink-0`}
|
||||
/>
|
||||
{t("article.progress-title", { current, total })}
|
||||
</>
|
||||
)}
|
||||
@ -117,10 +121,10 @@ function ArticleContent() {
|
||||
<>
|
||||
<GenerateProgress {...state} />
|
||||
{hash && (
|
||||
<div className={`flex flex-row items-center mb-6`}>
|
||||
<div className={`article-action flex flex-row items-center my-4 gap-4`}>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`mt-5 w-full mr-2`}
|
||||
className={`w-full whitespace-nowrap`}
|
||||
onClick={() => {
|
||||
location.href = `${apiEndpoint}/article/download/zip?hash=${hash}`;
|
||||
}}
|
||||
@ -131,7 +135,7 @@ function ArticleContent() {
|
||||
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`mt-5 w-full ml-2`}
|
||||
className={`w-full whitespace-nowrap`}
|
||||
onClick={() => {
|
||||
location.href = `${apiEndpoint}/article/download/tar?hash=${hash}`;
|
||||
}}
|
||||
|
@ -41,7 +41,7 @@ function SharingForm({ refer, data }: SharingFormProps) {
|
||||
</div>
|
||||
<div className={`body`}>
|
||||
{data.messages.map((message, i) => (
|
||||
<MessageSegment message={message} key={i} />
|
||||
<MessageSegment message={message} key={i} index={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className={`action`}>
|
||||
|
@ -12,10 +12,10 @@ export const settingsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState: {
|
||||
dialog: false,
|
||||
context: getBooleanMemory("context", true),
|
||||
align: getBooleanMemory("align", false),
|
||||
history: getNumberMemory("history_context", 8),
|
||||
sender: getBooleanMemory("sender", false),
|
||||
context: getBooleanMemory("context", true), // keep context
|
||||
align: getBooleanMemory("align", false), // chat textarea align center
|
||||
history: getNumberMemory("history_context", 8), // max history context length
|
||||
sender: getBooleanMemory("sender", false), // sender (false: Ctrl + Enter, true: Enter)
|
||||
},
|
||||
reducers: {
|
||||
toggleDialog: (state) => {
|
||||
|
@ -97,7 +97,6 @@ func ChatHandler(conn *Connection, user *auth.User, instance *conversation.Conve
|
||||
&adapter.ChatProps{
|
||||
Model: model,
|
||||
Message: segment,
|
||||
Plan: plan,
|
||||
Buffer: *buffer,
|
||||
},
|
||||
func(data string) error {
|
||||
|
@ -71,7 +71,6 @@ func getChatProps(form RelayForm, messages []globals.Message, buffer *utils.Buff
|
||||
return &adapter.ChatProps{
|
||||
Model: form.Model,
|
||||
Message: messages,
|
||||
Plan: plan,
|
||||
Token: utils.Multi(form.MaxTokens == 0, 2500, form.MaxTokens),
|
||||
PresencePenalty: form.PresencePenalty,
|
||||
FrequencyPenalty: form.FrequencyPenalty,
|
||||
|
@ -44,7 +44,6 @@ func NativeChatHandler(c *gin.Context, user *auth.User, model string, message []
|
||||
auth.GetGroup(db, user),
|
||||
&adapter.ChatProps{
|
||||
Model: model,
|
||||
Plan: plan,
|
||||
Message: segment,
|
||||
Buffer: *buffer,
|
||||
},
|
||||
|
@ -16,6 +16,8 @@ const (
|
||||
RestartType = "restart"
|
||||
ShareType = "share"
|
||||
MaskType = "mask"
|
||||
EditType = "edit"
|
||||
RemoveType = "remove"
|
||||
)
|
||||
|
||||
type Stack chan *conversation.FormMessage
|
||||
|
@ -297,3 +297,17 @@ func (c *Conversation) RemoveMessage(index int) globals.Message {
|
||||
func (c *Conversation) RemoveLatestMessage() globals.Message {
|
||||
return c.RemoveMessage(len(c.Message) - 1)
|
||||
}
|
||||
|
||||
func (c *Conversation) EditMessage(index int, message string) {
|
||||
if index < 0 || index >= len(c.Message) {
|
||||
return
|
||||
}
|
||||
c.Message[index].Content = message
|
||||
}
|
||||
|
||||
func (c *Conversation) DeleteMessage(index int) {
|
||||
if index < 0 || index >= len(c.Message) {
|
||||
return
|
||||
}
|
||||
c.Message = append(c.Message[:index], c.Message[index+1:]...)
|
||||
}
|
||||
|
@ -57,11 +57,10 @@ func ImagesRelayAPI(c *gin.Context) {
|
||||
createRelayImageObject(c, form, prompt, created, user, false)
|
||||
}
|
||||
|
||||
func getImageProps(form RelayImageForm, messages []globals.Message, buffer *utils.Buffer, plan bool) *adapter.ChatProps {
|
||||
func getImageProps(form RelayImageForm, messages []globals.Message, buffer *utils.Buffer) *adapter.ChatProps {
|
||||
return &adapter.ChatProps{
|
||||
Model: form.Model,
|
||||
Message: messages,
|
||||
Plan: plan,
|
||||
Token: 2500,
|
||||
Buffer: *buffer,
|
||||
}
|
||||
@ -90,7 +89,7 @@ func createRelayImageObject(c *gin.Context, form RelayImageForm, prompt string,
|
||||
}
|
||||
|
||||
buffer := utils.NewBuffer(form.Model, messages, channel.ChargeInstance.GetCharge(form.Model))
|
||||
err := channel.NewChatRequest(auth.GetGroup(db, user), getImageProps(form, messages, buffer, plan), func(data string) error {
|
||||
err := channel.NewChatRequest(auth.GetGroup(db, user), getImageProps(form, messages, buffer), func(data string) error {
|
||||
buffer.Write(data)
|
||||
return nil
|
||||
})
|
||||
|
@ -34,6 +34,25 @@ func ParseAuth(c *gin.Context, token string) *auth.User {
|
||||
return auth.ParseToken(c, token)
|
||||
}
|
||||
|
||||
func splitMessage(message string) (int, string, error) {
|
||||
parts := strings.SplitN(message, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
if id, err := strconv.Atoi(parts[0]); err == nil {
|
||||
return id, parts[1], nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, message, fmt.Errorf("message type error")
|
||||
}
|
||||
|
||||
func getId(message string) (int, error) {
|
||||
if id, err := strconv.Atoi(message); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("message type error")
|
||||
}
|
||||
|
||||
func ChatAPI(c *gin.Context) {
|
||||
var conn *utils.WebSocket
|
||||
if conn = utils.NewWebsocket(c, false); conn == nil {
|
||||
@ -84,6 +103,21 @@ func ChatAPI(c *gin.Context) {
|
||||
instance.SaveResponse(db, response)
|
||||
case MaskType:
|
||||
instance.LoadMask(form.Message)
|
||||
case EditType:
|
||||
if id, message, err := splitMessage(form.Message); err == nil {
|
||||
instance.EditMessage(id, message)
|
||||
instance.SaveConversation(db)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
case RemoveType:
|
||||
id, err := getId(form.Message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance.RemoveMessage(id)
|
||||
instance.SaveConversation(db)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
Loading…
Reference in New Issue
Block a user