mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 13:00:14 +09:00
feat: support rename conversation, support vision capability for claude-3 models and fix high context bug (#83)
This commit is contained in:
parent
f2bf2bdf76
commit
7cf6bd1ed8
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func formatMessages(props *ChatProps) interface{} {
|
||||
if globals.IsOpenAIVisionModels(props.Model) {
|
||||
if globals.IsVisionModel(props.Model) {
|
||||
return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {
|
||||
if message.Role == globals.User {
|
||||
raw, urls := utils.ExtractImages(message.Content, true)
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func formatMessages(props *ChatProps) interface{} {
|
||||
if globals.IsOpenAIVisionModels(props.Model) {
|
||||
if globals.IsVisionModel(props.Model) {
|
||||
return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {
|
||||
if message.Role == globals.User {
|
||||
content, urls := utils.ExtractImages(message.Content, true)
|
||||
|
@ -2,6 +2,7 @@ export type CommonResponse = {
|
||||
status: boolean;
|
||||
error?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
@ -17,6 +18,7 @@ export function toastState(
|
||||
else
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: state.error ?? state.reason ?? "error occurred",
|
||||
description:
|
||||
state.error ?? state.reason ?? state.message ?? "error occurred",
|
||||
});
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import axios from "axios";
|
||||
import type { ConversationInstance } from "./types.tsx";
|
||||
import { setHistory } from "@/store/chat.ts";
|
||||
import { AppDispatch } from "@/store";
|
||||
import { CommonResponse } from "@/api/common.ts";
|
||||
import { getErrorMessage } from "@/utils/base.ts";
|
||||
|
||||
export async function getConversationList(): Promise<ConversationInstance[]> {
|
||||
try {
|
||||
@ -46,6 +48,19 @@ export async function deleteConversation(id: number): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function renameConversation(
|
||||
id: number,
|
||||
name: string,
|
||||
): Promise<CommonResponse> {
|
||||
try {
|
||||
const resp = await axios.post("/conversation/rename", { id, name });
|
||||
return resp.data as CommonResponse;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return { status: false, error: getErrorMessage(e) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAllConversations(): Promise<boolean> {
|
||||
try {
|
||||
const resp = await axios.get("/conversation/clean");
|
||||
|
@ -149,6 +149,7 @@
|
||||
border-radius: var(--radius);
|
||||
text-align: center;
|
||||
font-size: 0.785rem;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
flex-shrink: 0;
|
||||
|
@ -445,11 +445,14 @@
|
||||
|
||||
.more {
|
||||
color: hsl(var(--text-secondary));
|
||||
display: none;
|
||||
scale: 0;
|
||||
opacity: 1;
|
||||
transition: 0.2s;
|
||||
opacity: 0;
|
||||
transition-property: color, opacity;
|
||||
border: 1px solid var(--border);
|
||||
outline: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--text));
|
||||
@ -461,12 +464,14 @@
|
||||
border-color: hsl(var(--border-hover));
|
||||
|
||||
.id {
|
||||
display: none;
|
||||
scale: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ function FileProvider({ value, onChange }: FileProviderProps) {
|
||||
);
|
||||
if (
|
||||
file.content.length > MaxPromptSize &&
|
||||
isHighContextModel(supportModels, model)
|
||||
!isHighContextModel(supportModels, model)
|
||||
) {
|
||||
file.content = file.content.slice(0, MaxPromptSize);
|
||||
toast({
|
||||
|
@ -126,7 +126,7 @@ function MessageContent({ message, end, index, onEvent }: MessageProps) {
|
||||
isUser ? (
|
||||
<Avatar className={`message-avatar`} username={username} />
|
||||
) : (
|
||||
<img src={appLogo} alt={``} className={`message-avatar p-1`} />
|
||||
<img src={appLogo} alt={``} className={`message-avatar`} />
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
MoreHorizontal,
|
||||
PencilLine,
|
||||
Share2,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
@ -20,6 +21,9 @@ import { ConversationInstance } from "@/api/types.tsx";
|
||||
import { useState } from "react";
|
||||
import { closeMarket, useConversationActions } from "@/store/chat.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx";
|
||||
import { toastState } from "@/api/common.ts";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
|
||||
type ConversationSegmentProps = {
|
||||
conversation: ConversationInstance;
|
||||
@ -37,9 +41,13 @@ function ConversationSegment({
|
||||
const dispatch = useDispatch();
|
||||
const { toggle } = useConversationActions();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { rename } = useConversationActions();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const [editDialog, setEditDialog] = useState(false);
|
||||
|
||||
const loading = conversation.id <= 0;
|
||||
|
||||
return (
|
||||
@ -59,13 +67,6 @@ function ConversationSegment({
|
||||
>
|
||||
<MessageSquare className={`h-4 w-4 mr-1`} />
|
||||
<div className={`title`}>{filterMessage(conversation.name)}</div>
|
||||
<div className={cn("id", loading && "loading")}>
|
||||
{loading ? (
|
||||
<Loader2 className={`mr-0.5 h-4 w-4 animate-spin`} />
|
||||
) : (
|
||||
conversation.id
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={(state: boolean) => {
|
||||
@ -74,20 +75,54 @@ function ConversationSegment({
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
className={`outline-none`}
|
||||
className={`flex flex-row outline-none`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className={cn("id", loading && "loading")}>
|
||||
{loading ? (
|
||||
<Loader2 className={`mr-0.5 h-4 w-4 animate-spin`} />
|
||||
) : (
|
||||
conversation.id
|
||||
)}
|
||||
</div>
|
||||
<MoreHorizontal className={`more h-5 w-5 p-0.5`} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<PopupDialog
|
||||
title={t("conversation.edit-title")}
|
||||
open={editDialog}
|
||||
setOpen={setEditDialog}
|
||||
type={popupTypes.Text}
|
||||
name={t("title")}
|
||||
defaultValue={conversation.name}
|
||||
onSubmit={async (name) => {
|
||||
const resp = await rename(conversation.id, name);
|
||||
toastState(toast, t, resp, true);
|
||||
if (!resp.status) return false;
|
||||
|
||||
setEditDialog(false);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
// prevent click event from opening the dropdown menu
|
||||
if (offset + 500 > new Date().getTime()) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setEditDialog(true);
|
||||
}}
|
||||
>
|
||||
<PencilLine className={`h-4 w-4 mx-1`} />
|
||||
{t("conversation.edit-title")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
operate({ target: conversation, type: "delete" });
|
||||
@ -95,7 +130,7 @@ function ConversationSegment({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash2 className={`more h-4 w-4 mx-1`} />
|
||||
<Trash2 className={`h-4 w-4 mx-1`} />
|
||||
{t("conversation.delete-conversation")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@ -107,7 +142,7 @@ function ConversationSegment({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Share2 className={`more h-4 w-4 mx-1`} />
|
||||
<Share2 className={`h-4 w-4 mx-1`} />
|
||||
{t("share.share-conversation")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
@ -51,6 +51,7 @@
|
||||
"model": "模型",
|
||||
"min-quota": "最低余额",
|
||||
"your-quota": "您的余额",
|
||||
"title": "标题",
|
||||
"auth": {
|
||||
"username": "用户名",
|
||||
"username-placeholder": "请输入用户名",
|
||||
@ -126,6 +127,7 @@
|
||||
"remove-all-description": "此操作无法撤消。这将永久删除所有对话,是否继续?",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"edit-title": "编辑标题",
|
||||
"delete-conversation": "删除对话",
|
||||
"delete-success": "对话已删除",
|
||||
"delete-success-prompt": "对话已删除。",
|
||||
|
@ -75,7 +75,8 @@
|
||||
"delete-success": "Conversation deleted",
|
||||
"delete-success-prompt": "Conversation has been deleted.",
|
||||
"delete-failed": "Delete failed",
|
||||
"delete-failed-prompt": "Failed to delete conversation. Please check your network and try again."
|
||||
"delete-failed-prompt": "Failed to delete conversation. Please check your network and try again.",
|
||||
"edit-title": "Edit Title"
|
||||
},
|
||||
"chat": {
|
||||
"web": "Web Searching",
|
||||
@ -746,5 +747,6 @@
|
||||
"exit": "Leave",
|
||||
"model": "Model",
|
||||
"min-quota": "Minimum Balance",
|
||||
"your-quota": "Your balance"
|
||||
"your-quota": "Your balance",
|
||||
"title": "Title"
|
||||
}
|
@ -75,7 +75,8 @@
|
||||
"delete-success": "会話が削除されました",
|
||||
"delete-success-prompt": "会話が削除されました。",
|
||||
"delete-failed": "削除失敗",
|
||||
"delete-failed-prompt": "会話を削除できませんでした。ネットワークを確認して、もう一度お試しください。"
|
||||
"delete-failed-prompt": "会話を削除できませんでした。ネットワークを確認して、もう一度お試しください。",
|
||||
"edit-title": "タイトルを編集..."
|
||||
},
|
||||
"chat": {
|
||||
"web": "インターネット検索",
|
||||
@ -746,5 +747,6 @@
|
||||
"exit": "離れる",
|
||||
"model": "モデル",
|
||||
"min-quota": "最低残高",
|
||||
"your-quota": "残高"
|
||||
"your-quota": "残高",
|
||||
"title": "タイトル"
|
||||
}
|
@ -75,7 +75,8 @@
|
||||
"delete-success": "Разговор удален",
|
||||
"delete-success-prompt": "Разговор был удален.",
|
||||
"delete-failed": "Ошибка удаления",
|
||||
"delete-failed-prompt": "Не удалось удалить разговор. Пожалуйста, проверьте свою сеть и попробуйте еще раз."
|
||||
"delete-failed-prompt": "Не удалось удалить разговор. Пожалуйста, проверьте свою сеть и попробуйте еще раз.",
|
||||
"edit-title": "Редактировать заголовок"
|
||||
},
|
||||
"chat": {
|
||||
"web": "Веб-поиск",
|
||||
@ -746,5 +747,6 @@
|
||||
"exit": "Закрыть",
|
||||
"model": "модель",
|
||||
"min-quota": "Минимальный баланс",
|
||||
"your-quota": "Ваш баланс"
|
||||
"your-quota": "Ваш баланс",
|
||||
"title": "заглавие"
|
||||
}
|
@ -22,6 +22,7 @@ import {
|
||||
import {
|
||||
deleteConversation as doDeleteConversation,
|
||||
deleteAllConversations as doDeleteAllConversations,
|
||||
renameConversation as doRenameConversation,
|
||||
loadConversation,
|
||||
getConversationList,
|
||||
} from "@/api/history.ts";
|
||||
@ -257,6 +258,11 @@ const chatSlice = createSlice({
|
||||
// add a new history at the beginning
|
||||
state.history = [{ id: -1, name, message: [] }, ...state.history];
|
||||
},
|
||||
renameHistory: (state, action) => {
|
||||
const { id, name } = action.payload as { id: number; name: string };
|
||||
const conversation = state.history.find((item) => item.id === id);
|
||||
if (conversation) conversation.name = name;
|
||||
},
|
||||
setModel: (state, action) => {
|
||||
setMemory("model", action.payload as string);
|
||||
state.model = action.payload as string;
|
||||
@ -355,6 +361,7 @@ const chatSlice = createSlice({
|
||||
|
||||
export const {
|
||||
setHistory,
|
||||
renameHistory,
|
||||
setCurrent,
|
||||
setModel,
|
||||
setWeb,
|
||||
@ -441,6 +448,12 @@ export function useConversationActions() {
|
||||
|
||||
dispatch(setCurrent(id));
|
||||
},
|
||||
rename: async (id: number, name: string) => {
|
||||
const resp = await doRenameConversation(id, name);
|
||||
resp.status && dispatch(renameHistory({ id, name }));
|
||||
|
||||
return resp;
|
||||
},
|
||||
remove: async (id: number) => {
|
||||
const state = await doDeleteConversation(id);
|
||||
state && dispatch(deleteConversation(id));
|
||||
|
@ -75,6 +75,7 @@ const (
|
||||
Claude2 = "claude-1-100k"
|
||||
Claude2100k = "claude-2"
|
||||
Claude2200k = "claude-2.1"
|
||||
Claude3 = "claude-3"
|
||||
ClaudeSlack = "claude-slack"
|
||||
SparkDesk = "spark-desk-v1.5"
|
||||
SparkDeskV2 = "spark-desk-v2"
|
||||
@ -109,14 +110,10 @@ var OpenAIDalleModels = []string{
|
||||
Dalle, Dalle2, Dalle3,
|
||||
}
|
||||
|
||||
var OpenAIVisionModels = []string{
|
||||
//GPT4Vision, GPT4All, GPT4Dalle,
|
||||
GPT4VisionPreview, GPT41106VisionPreview,
|
||||
}
|
||||
|
||||
var VisionModels = []string{
|
||||
GPT4VisionPreview, GPT41106VisionPreview,
|
||||
GeminiProVision,
|
||||
GPT4VisionPreview, GPT41106VisionPreview, // openai
|
||||
GeminiProVision, // gemini
|
||||
Claude3, // anthropic
|
||||
}
|
||||
|
||||
func in(value string, slice []string) bool {
|
||||
@ -133,11 +130,6 @@ func IsOpenAIDalleModel(model string) bool {
|
||||
return in(model, OpenAIDalleModels) && !strings.Contains(model, "gpt-4-dalle")
|
||||
}
|
||||
|
||||
func IsOpenAIVisionModels(model string) bool {
|
||||
// enable openai image format for gpt-4-vision-preview models
|
||||
return in(model, OpenAIVisionModels)
|
||||
}
|
||||
|
||||
func IsVisionModel(model string) bool {
|
||||
return in(model, VisionModels)
|
||||
}
|
||||
|
@ -14,6 +14,11 @@ type ShareForm struct {
|
||||
Refs []int `json:"refs"`
|
||||
}
|
||||
|
||||
type RenameConversationForm struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type DeleteMaskForm struct {
|
||||
Id int `json:"id" binding:"required"`
|
||||
}
|
||||
@ -116,6 +121,41 @@ func DeleteAPI(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func RenameAPI(c *gin.Context) {
|
||||
user := auth.GetUser(c)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "user not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := utils.GetDBFromContext(c)
|
||||
var form RenameConversationForm
|
||||
if err := c.ShouldBindJSON(&form); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "invalid form",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
conversation := LoadConversation(db, user.GetID(db), form.Id)
|
||||
if conversation == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": "conversation not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
conversation.RenameConversation(db, form.Name)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
||||
|
||||
func CleanAPI(c *gin.Context) {
|
||||
user := auth.GetUser(c)
|
||||
if user == nil {
|
||||
|
@ -7,6 +7,7 @@ func Register(app *gin.RouterGroup) {
|
||||
{
|
||||
router.GET("/list", ListAPI)
|
||||
router.GET("/load", LoadAPI)
|
||||
router.POST("/rename", RenameAPI)
|
||||
router.GET("/delete", DeleteAPI)
|
||||
router.GET("/clean", CleanAPI)
|
||||
|
||||
|
@ -115,6 +115,14 @@ func (c *Conversation) DeleteConversation(db *sql.DB) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conversation) RenameConversation(db *sql.DB, name string) bool {
|
||||
_, err := globals.ExecDb(db, "UPDATE conversation SET conversation_name = ? WHERE user_id = ? AND conversation_id = ?", name, c.UserID, c.Id)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func DeleteAllConversations(db *sql.DB, user auth.User) error {
|
||||
_, err := globals.ExecDb(db, "DELETE FROM conversation WHERE user_id = ?", user.GetID(db))
|
||||
return err
|
||||
|
@ -113,7 +113,7 @@ func (i *Image) GetPixelColor(x int, y int) (int, int, int) {
|
||||
}
|
||||
|
||||
func (i *Image) CountTokens(model string) int {
|
||||
if globals.IsOpenAIVisionModels(model) {
|
||||
if globals.IsVisionModel(model) {
|
||||
// tile size is 512x512
|
||||
// the max size of image is 2048x2048
|
||||
// the image that is larger than 2048x2048 will be resized in 16 tiles
|
||||
|
Loading…
Reference in New Issue
Block a user