allow conversation with some document (.pdf, .txt)

This commit is contained in:
josc146 2023-10-27 11:36:29 +08:00
parent 810843a5ab
commit c87de93498
10 changed files with 195 additions and 25 deletions

View File

@ -53,12 +53,12 @@ type FileInfo struct {
ModTime string `json:"modTime"` ModTime string `json:"modTime"`
} }
func (a *App) ReadFileInfo(fileName string) (FileInfo, error) { func (a *App) ReadFileInfo(fileName string) (*FileInfo, error) {
info, err := os.Stat(a.exDir + fileName) info, err := os.Stat(a.exDir + fileName)
if err != nil { if err != nil {
return FileInfo{}, err return nil, err
} }
return FileInfo{ return &FileInfo{
Name: info.Name(), Name: info.Name(),
Size: info.Size(), Size: info.Size(),
IsDir: info.IsDir(), IsDir: info.IsDir(),
@ -145,6 +145,20 @@ func (a *App) OpenSaveFileDialogBytes(filterPattern string, defaultFileName stri
return path, nil return path, nil
} }
// Only return the path of the selected file, because communication between frontend and backend is slow. Use AssetServer Handler to read the file.
func (a *App) OpenOpenFileDialog(filterPattern string) (string, error) {
path, err := wruntime.OpenFileDialog(a.ctx, wruntime.OpenDialogOptions{
Filters: []wruntime.FileFilter{{Pattern: filterPattern}},
})
if err != nil {
return "", err
}
if path == "" {
return "", nil
}
return path, nil
}
func (a *App) OpenFileFolder(path string, relative bool) error { func (a *App) OpenFileFolder(path string, relative bool) error {
var absPath string var absPath string
var err error var err error

View File

@ -58,17 +58,22 @@ async def file_to_text(
file_parsers = {".txt": parse_text, ".pdf": parse_pdf} file_parsers = {".txt": parse_text, ".pdf": parse_pdf}
file_ext = os.path.splitext(params.file_name)[-1] file_name = file_data.filename or params.file_name
file_ext = os.path.splitext(file_name)[-1]
if file_ext not in file_parsers: if file_ext not in file_parsers:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "file type not supported") raise HTTPException(status.HTTP_400_BAD_REQUEST, "file type not supported")
pages: Iterator[Document] = file_parsers[file_ext]( try:
Blob.from_data( pages: Iterator[Document] = file_parsers[file_ext](
await file_data.read(), Blob.from_data(
encoding=params.file_encoding, await file_data.read(),
path=params.file_name, encoding=params.file_encoding,
path=file_name,
)
) )
) pages = list(pages)
except Exception as e:
raise HTTPException(status.HTTP_400_BAD_REQUEST, f"{e}")
return {"pages": pages} return {"pages": pages}

View File

@ -254,5 +254,12 @@
"User Name": "ユーザー名", "User Name": "ユーザー名",
"Assistant Name": "アシスタント名", "Assistant Name": "アシスタント名",
"Insert default system prompt at the beginning": "最初にデフォルトのシステムプロンプトを挿入", "Insert default system prompt at the beginning": "最初にデフォルトのシステムプロンプトを挿入",
"Format Content": "内容フォーマットの規格化" "Format Content": "内容フォーマットの規格化",
"Add An Attachment (Accepts pdf, txt)": "添付ファイルを追加 (pdf, txtを受け付けます)",
"Uploading Attachment": "添付ファイルアップロード中",
"Remove Attachment": "添付ファイルを削除",
"The content of file": "ファイル",
"is as follows. When replying to me, consider the file content and respond accordingly:": "の内容は以下の通りです。私に返信する際は、ファイルの内容を考慮して適切に返信してください:",
"What's the file name": "ファイル名は何ですか",
"The file name is: ": "ファイル名は次のとおりです: "
} }

View File

@ -254,5 +254,12 @@
"User Name": "用户名称", "User Name": "用户名称",
"Assistant Name": "AI名称", "Assistant Name": "AI名称",
"Insert default system prompt at the beginning": "在开头自动插入默认系统提示", "Insert default system prompt at the beginning": "在开头自动插入默认系统提示",
"Format Content": "规范格式" "Format Content": "规范格式",
"Add An Attachment (Accepts pdf, txt)": "添加一个附件 (支持pdf, txt)",
"Uploading Attachment": "正在上传附件",
"Remove Attachment": "移除附件",
"The content of file": "文件",
"is as follows. When replying to me, consider the file content and respond accordingly:": "内容如下。回复时考虑文件内容并做出相应回复:",
"What's the file name": "文件名是什么",
"The file name is: ": "文件名是:"
} }

View File

@ -10,14 +10,22 @@ import { KebabHorizontalIcon, PencilIcon, SyncIcon, TrashIcon } from '@primer/oc
import logo from '../assets/images/logo.png'; import logo from '../assets/images/logo.png';
import MarkdownRender from '../components/MarkdownRender'; import MarkdownRender from '../components/MarkdownRender';
import { ToolTipButton } from '../components/ToolTipButton'; import { ToolTipButton } from '../components/ToolTipButton';
import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular, Save28Regular } from '@fluentui/react-icons'; import {
ArrowCircleUp28Regular,
ArrowClockwise16Regular,
Attach16Regular,
Delete28Regular,
Dismiss16Regular,
RecordStop28Regular,
Save28Regular
} from '@fluentui/react-icons';
import { CopyButton } from '../components/CopyButton'; import { CopyButton } from '../components/CopyButton';
import { ReadButton } from '../components/ReadButton'; import { ReadButton } from '../components/ReadButton';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { WorkHeader } from '../components/WorkHeader'; import { WorkHeader } from '../components/WorkHeader';
import { DialogButton } from '../components/DialogButton'; import { DialogButton } from '../components/DialogButton';
import { OpenFileFolder, OpenSaveFileDialog } from '../../wailsjs/go/backend_golang/App'; import { OpenFileFolder, OpenOpenFileDialog, OpenSaveFileDialog } from '../../wailsjs/go/backend_golang/App';
import { toastWithButton } from '../utils'; import { bytesToReadable, toastWithButton } from '../utils';
import { PresetsButton } from './PresetsManager/PresetsButton'; import { PresetsButton } from './PresetsManager/PresetsButton';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
@ -267,6 +275,16 @@ const ChatPanel: FC = observer(() => {
let targetRange = commonStore.conversationOrder.slice(startIndex, endIndex); let targetRange = commonStore.conversationOrder.slice(startIndex, endIndex);
const messages: ConversationMessage[] = []; const messages: ConversationMessage[] = [];
if (commonStore.attachmentContent) {
messages.push({
role: 'user',
content: t('The content of file') + ` "${commonStore.attachmentName}" `
+ t('is as follows. When replying to me, consider the file content and respond accordingly:')
+ '\n\n' + commonStore.attachmentContent
});
messages.push({ role: 'user', content: t('What\'s the file name') });
messages.push({ role: 'assistant', content: t('The file name is: ') + commonStore.attachmentName });
}
targetRange.forEach((uuid, index) => { targetRange.forEach((uuid, index) => {
if (uuid === welcomeUuid) if (uuid === welcomeUuid)
return; return;
@ -385,16 +403,101 @@ const ChatPanel: FC = observer(() => {
commonStore.setConversation({}); commonStore.setConversation({});
commonStore.setConversationOrder([]); commonStore.setConversationOrder([]);
}} /> }} />
<Textarea <div className="relative flex grow">
ref={inputRef} <Textarea
style={{ minWidth: 0 }} ref={inputRef}
className="grow" style={{ minWidth: 0 }}
resize="vertical" className="grow"
placeholder={t('Type your message here')!} resize="vertical"
value={commonStore.currentInput} placeholder={t('Type your message here')!}
onChange={(e) => commonStore.setCurrentInput(e.target.value)} value={commonStore.currentInput}
onKeyDown={handleKeyDownOrClick} onChange={(e) => commonStore.setCurrentInput(e.target.value)}
/> onKeyDown={handleKeyDownOrClick}
/>
<div className="absolute right-2 bottom-2">
{!commonStore.attachmentContent ?
<ToolTipButton
desc={commonStore.attachmentUploading ?
t('Uploading Attachment') :
t('Add An Attachment (Accepts pdf, txt)')}
icon={commonStore.attachmentUploading ?
<ArrowClockwise16Regular className="animate-spin" />
: <Attach16Regular />}
size="small" shape="circular" appearance="secondary"
onClick={() => {
if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl) {
toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' });
return;
}
if (commonStore.attachmentUploading)
return;
OpenOpenFileDialog('*.txt;*.pdf').then(async filePath => {
if (!filePath)
return;
commonStore.setAttachmentUploading(true);
// Both are slow. Communication between frontend and backend is slow. Use AssetServer Handler to read the file.
// const blob = new Blob([atob(info.content as unknown as string)]); // await fetch(`data:application/octet-stream;base64,${info.content}`).then(r => r.blob());
const blob = await fetch(`=>${filePath}`).then(r => r.blob());
const attachmentName = filePath.split(/[\\/]/).pop();
const urlPath = `/file-to-text?file_name=${attachmentName}`;
const bodyForm = new FormData();
bodyForm.append('file_data', blob, attachmentName);
fetch(commonStore.settings.apiUrl ?
commonStore.settings.apiUrl + urlPath :
`http://127.0.0.1:${port}${urlPath}`, {
method: 'POST',
body: bodyForm
}).then(async r => {
if (r.status === 200) {
const pages = (await r.json()).pages as any[];
let attachmentContent: string;
if (pages.length === 1)
attachmentContent = pages[0].page_content;
else
attachmentContent = pages.map((p, i) => `Page ${i + 1}:\n${p.page_content}`).join('\n\n');
commonStore.setAttachmentName(attachmentName!);
commonStore.setAttachmentSize(blob.size);
commonStore.setAttachmentContent(attachmentContent);
} else {
toast(r.statusText + '\n' + (await r.text()), {
type: 'error'
});
}
commonStore.setAttachmentUploading(false);
}
).catch(e => {
commonStore.setAttachmentUploading(false);
toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 });
});
}).catch(e => {
toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 });
});
}}
/> :
<div>
<ToolTipButton
text={
commonStore.attachmentName.replace(
new RegExp('(^[^\\.]{5})[^\\.]+'), '$1...')
}
desc={`${commonStore.attachmentName} (${bytesToReadable(commonStore.attachmentSize)})`}
size="small" shape="circular" appearance="secondary" />
<ToolTipButton desc={t('Remove Attachment')}
icon={<Dismiss16Regular />}
size="small" shape="circular" appearance="subtle"
onClick={() => {
commonStore.setAttachmentName('');
commonStore.setAttachmentSize(0);
commonStore.setAttachmentContent('');
}} />
</div>
}
</div>
</div>
<ToolTipButton desc={generating ? t('Stop') : t('Send')} <ToolTipButton desc={generating ? t('Stop') : t('Send')}
icon={generating ? <RecordStop28Regular /> : <ArrowCircleUp28Regular />} icon={generating ? <RecordStop28Regular /> : <ArrowCircleUp28Regular />}
size={mq ? 'large' : 'small'} shape="circular" appearance="subtle" size={mq ? 'large' : 'small'} shape="circular" appearance="subtle"

View File

@ -54,6 +54,10 @@ class CommonStore {
conversation: Conversation = {}; conversation: Conversation = {};
conversationOrder: string[] = []; conversationOrder: string[] = [];
activePreset: Preset | null = null; activePreset: Preset | null = null;
attachmentUploading: boolean = false;
attachmentName: string = '';
attachmentSize: number = 0;
attachmentContent: string = '';
// completion // completion
completionPreset: CompletionPreset | null = null; completionPreset: CompletionPreset | null = null;
completionGenerating: boolean = false; completionGenerating: boolean = false;
@ -325,6 +329,22 @@ class CommonStore {
setLoraModels(value: string[]) { setLoraModels(value: string[]) {
this.loraModels = value; this.loraModels = value;
} }
setAttachmentUploading(value: boolean) {
this.attachmentUploading = value;
}
setAttachmentName(value: string) {
this.attachmentName = value;
}
setAttachmentSize(value: number) {
this.attachmentSize = value;
}
setAttachmentContent(value: string) {
this.attachmentContent = value;
}
} }
export default new CommonStore(); export default new CommonStore();

View File

@ -282,6 +282,13 @@ export function bytesToKb(size: number) {
return (size / 1024).toFixed(2); return (size / 1024).toFixed(2);
} }
export function bytesToReadable(size: number) {
if (size < 1024) return size + ' B';
else if (size < 1024 * 1024) return bytesToKb(size) + ' KB';
else if (size < 1024 * 1024 * 1024) return bytesToMb(size) + ' MB';
else return bytesToGb(size) + ' GB';
}
export async function checkUpdate(notifyEvenLatest: boolean = false) { export async function checkUpdate(notifyEvenLatest: boolean = false) {
fetch(!commonStore.settings.giteeUpdatesSource ? fetch(!commonStore.settings.giteeUpdatesSource ?
'https://api.github.com/repos/josstorer/RWKV-Runner/releases/latest' : 'https://api.github.com/repos/josstorer/RWKV-Runner/releases/latest' :

View File

@ -34,6 +34,8 @@ export function MergeLora(arg1:string,arg2:boolean,arg3:number,arg4:string,arg5:
export function OpenFileFolder(arg1:string,arg2:boolean):Promise<void>; export function OpenFileFolder(arg1:string,arg2:boolean):Promise<void>;
export function OpenOpenFileDialog(arg1:string):Promise<string>;
export function OpenSaveFileDialog(arg1:string,arg2:string,arg3:string):Promise<string>; export function OpenSaveFileDialog(arg1:string,arg2:string,arg3:string):Promise<string>;
export function OpenSaveFileDialogBytes(arg1:string,arg2:string,arg3:Array<number>):Promise<string>; export function OpenSaveFileDialogBytes(arg1:string,arg2:string,arg3:Array<number>):Promise<string>;

View File

@ -66,6 +66,10 @@ export function OpenFileFolder(arg1, arg2) {
return window['go']['backend_golang']['App']['OpenFileFolder'](arg1, arg2); return window['go']['backend_golang']['App']['OpenFileFolder'](arg1, arg2);
} }
export function OpenOpenFileDialog(arg1) {
return window['go']['backend_golang']['App']['OpenOpenFileDialog'](arg1);
}
export function OpenSaveFileDialog(arg1, arg2, arg3) { export function OpenSaveFileDialog(arg1, arg2, arg3) {
return window['go']['backend_golang']['App']['OpenSaveFileDialog'](arg1, arg2, arg3); return window['go']['backend_golang']['App']['OpenSaveFileDialog'](arg1, arg2, arg3);
} }

View File

@ -27,6 +27,7 @@ func NewFileLoader() *FileLoader {
func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) { func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) {
var err error var err error
requestedFilename := strings.TrimPrefix(req.URL.Path, "/") requestedFilename := strings.TrimPrefix(req.URL.Path, "/")
requestedFilename = strings.TrimPrefix(requestedFilename, "=>") // absolute path
println("Requesting file:", requestedFilename) println("Requesting file:", requestedFilename)
fileData, err := os.ReadFile(requestedFilename) fileData, err := os.ReadFile(requestedFilename)
if err != nil { if err != nil {