allow reading attachments even if the model is offline

This commit is contained in:
josc146 2023-11-24 16:25:21 +08:00
parent 645e8e2f44
commit 177b2c54d9
8 changed files with 879 additions and 89 deletions

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"i18next": "^22.4.15",
"mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3",
"pdfjs-dist": "^4.0.189",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^5.2.0",
@ -50,6 +51,7 @@
"sass": "^1.62.1",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.4",
"vite": "^4.3.6"
"vite": "^4.3.6",
"vite-plugin-top-level-await": "^1.3.1"
}
}

View File

@ -271,5 +271,6 @@
"You can increase the number of stored layers in Configs page to improve performance": "パフォーマンスを向上させるために、保存されるレイヤーの数を設定ページで増やすことができます",
"Failed to load model, try to increase the virtual memory (Swap of WSL) or use a smaller base model.": "モデルの読み込みに失敗しました、仮想メモリ (WSL Swap) を増やすか小さなベースモデルを使用してみてください。",
"Save Conversation": "会話を保存",
"Use Hugging Face Mirror": "Hugging Faceミラーを使用"
"Use Hugging Face Mirror": "Hugging Faceミラーを使用",
"File is empty": "ファイルが空です"
}

View File

@ -271,5 +271,6 @@
"You can increase the number of stored layers in Configs page to improve performance": "你可以在配置页面增加载入显存层数以提升性能",
"Failed to load model, try to increase the virtual memory (Swap of WSL) or use a smaller base model.": "模型载入失败,尝试增加虚拟内存(WSL Swap),或使用一个更小规模的基底模型",
"Save Conversation": "保存对话",
"Use Hugging Face Mirror": "使用Hugging Face镜像源"
"Use Hugging Face Mirror": "使用Hugging Face镜像源",
"File is empty": "文件为空"
}

View File

@ -34,6 +34,7 @@ import { botName, ConversationMessage, MessageType, userName, welcomeUuid } from
import { Labeled } from '../components/Labeled';
import { ValuedSlider } from '../components/ValuedSlider';
import { PresetsButton } from './PresetsManager/PresetsButton';
import { webOpenOpenFileDialog } from '../utils/web-file-operations';
let chatSseControllers: {
[id: string]: AbortController
@ -560,43 +561,42 @@ const ChatPanel: FC = observer(() => {
: <Attach16Regular />}
size="small" shape="circular" appearance="secondary"
onClick={() => {
if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl && commonStore.platform !== 'web') {
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;
const filterPattern = '*.txt;*.pdf';
const setUploading = () => commonStore.setAttachmentUploading(true);
// actually, status of web platform is always Offline
if (commonStore.platform === 'web' || commonStore.status.status === ModelStatus.Offline) {
webOpenOpenFileDialog({ filterPattern, fnStartLoading: setUploading }).then(webReturn => {
if (webReturn.content)
commonStore.setCurrentTempAttachment(
{
name: webReturn.blob.name,
size: webReturn.blob.size,
content: webReturn.content
});
else
toast(t('File is empty'), {
type: 'info',
autoClose: 1000
});
}).catch(e => {
toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 });
}).finally(() => {
commonStore.setAttachmentUploading(false);
});
} else {
OpenOpenFileDialog(filterPattern).then(async filePath => {
if (!filePath)
return;
commonStore.setAttachmentUploading(true);
setUploading();
let blob: Blob;
let attachmentName: string | undefined;
let attachmentContent: string | undefined;
if (commonStore.platform === 'web') {
const webReturn = filePath as any;
blob = webReturn.blob;
attachmentName = blob.name;
attachmentContent = webReturn.content;
} else {
// 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());
blob = await fetch(absPathAsset(filePath)).then(r => r.blob());
attachmentName = filePath.split(/[\\/]/).pop();
}
if (attachmentContent) {
commonStore.setCurrentTempAttachment(
{
name: attachmentName!,
size: blob.size,
content: attachmentContent
});
commonStore.setAttachmentUploading(false);
} else {
const blob = await fetch(absPathAsset(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);
@ -606,31 +606,38 @@ const ChatPanel: FC = observer(() => {
}).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.setCurrentTempAttachment(
{
name: attachmentName!,
size: blob.size,
content: attachmentContent!
if (attachmentContent)
commonStore.setCurrentTempAttachment(
{
name: attachmentName!,
size: blob.size,
content: attachmentContent!
});
else
toast(t('File is empty'), {
type: 'info',
autoClose: 1000
});
} 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 });
}).finally(() => {
commonStore.setAttachmentUploading(false);
});
}
}).catch(e => {
toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 });
});
}).catch(e => {
toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 });
});
}
}}
/> :
<div>

View File

@ -0,0 +1,72 @@
import { getDocument, GlobalWorkerOptions, PDFDocumentProxy } from 'pdfjs-dist';
import { TextItem } from 'pdfjs-dist/types/src/display/api';
export function webOpenOpenFileDialog({ filterPattern, fnStartLoading }: { filterPattern: string, fnStartLoading: Function | null }): Promise<{ blob: Blob, content?: string }> {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = filterPattern
.replaceAll('*.txt', 'text/plain')
.replaceAll('*.', 'application/')
.replaceAll(';', ',');
input.onchange = async e => {
// @ts-ignore
const file: Blob = e.target?.files[0];
if (fnStartLoading && typeof fnStartLoading === 'function')
fnStartLoading();
if (!GlobalWorkerOptions.workerSrc)
// @ts-ignore
GlobalWorkerOptions.workerSrc = await import('pdfjs-dist/build/pdf.worker.min.mjs');
if (file.type === 'text/plain') {
const reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = event => {
const content = event.target?.result as string;
resolve({
blob: file,
content: content
});
};
reader.onerror = reject;
} else if (file.type === 'application/pdf') {
const readPDFPage = async (doc: PDFDocumentProxy, pageNo: number) => {
const page = await doc.getPage(pageNo);
const tokenizedText = await page.getTextContent();
return tokenizedText.items.map(token => (token as TextItem).str).join('');
};
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async (event) => {
try {
const doc = await getDocument(event.target?.result!).promise;
const pageTextPromises = [];
for (let pageNo = 1; pageNo <= doc.numPages; pageNo++) {
pageTextPromises.push(readPDFPage(doc, pageNo));
}
const pageTexts = await Promise.all(pageTextPromises);
let content;
if (pageTexts.length === 1)
content = pageTexts[0];
else
content = pageTexts.map((p, i) => `Page ${i + 1}:\n${p}`).join('\n\n');
resolve({
blob: file,
content: content
});
} catch (err) {
reject(err);
}
};
reader.onerror = reject;
} else {
resolve({
blob: file
});
}
};
input.click();
});
}

View File

@ -1,3 +1,5 @@
import { webOpenOpenFileDialog } from './utils/web-file-operations'
function defineRuntime(name, func) {
window.runtime[name] = func
}
@ -107,37 +109,7 @@ if (!window.go) {
defineApp('ListDirFiles', async () => {
return []
})
defineApp('OpenOpenFileDialog', async (filterPattern) => {
return new Promise((resolve, reject) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = filterPattern
.replaceAll('*.txt', 'text/plain')
.replaceAll('*.', 'application/')
.replaceAll(';', ',')
input.onchange = e => {
const file = e.target?.files[0]
if (file.type === 'text/plain') {
const reader = new FileReader()
reader.readAsText(file, 'UTF-8')
reader.onload = readerEvent => {
const content = readerEvent.target?.result
resolve({
blob: file,
content: content
})
}
} else {
resolve({
blob: file
})
}
}
input.click()
})
})
defineApp('OpenOpenFileDialog', webOpenOpenFileDialog)
defineApp('OpenSaveFileDialog', async (filterPattern, defaultFileName, savedContent) => {
const saver = await import('file-saver')
saver.saveAs(new Blob([savedContent], { type: 'text/plain;charset=utf-8' }), defaultFileName)

View File

@ -3,6 +3,7 @@ import { dependencies } from './package.json';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import topLevelAwait from 'vite-plugin-top-level-await';
// dependencies that exist anywhere
const vendor = [
@ -35,12 +36,18 @@ function renderChunks(deps: Record<string, string>) {
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(),
plugins: [
react(),
visualizer({
template: 'treemap',
gzipSize: true,
brotliSize: true
})],
}),
topLevelAwait({
promiseExportName: '__tla',
promiseImportName: i => `__tla_${i}`
})
],
build: {
chunkSizeWarningLimit: 3000,
rollupOptions: {