feat: support rename conversation, support vision capability for claude-3 models and fix high context bug (#83)

This commit is contained in:
Zhang Minghan 2024-03-06 09:31:01 +08:00
parent f2bf2bdf76
commit 7cf6bd1ed8
19 changed files with 159 additions and 39 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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",
});
}

View File

@ -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");

View File

@ -149,6 +149,7 @@
border-radius: var(--radius);
text-align: center;
font-size: 0.785rem;
padding: 0.125rem;
}
flex-shrink: 0;

View File

@ -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;
}
}

View File

@ -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({

View File

@ -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`} />
)
}
>

View File

@ -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>

View File

@ -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": "对话已删除。",

View File

@ -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"
}

View File

@ -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": "タイトル"
}

View File

@ -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": "заглавие"
}

View File

@ -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));

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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