feat: 批量上传支持及prompt优化
- 支持多文件批量上传,一次最多10个 - 新增json/toml/yaml格式支持 - 优化inline补全prompt结构,增加边界决策指南 - size计算包含doc_block内容长度 - 超限时显示警告tooltip
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
</svg>
|
||||
<span class="btn-tooltip">{{ t('uploadFile') }}</span>
|
||||
</button>
|
||||
<input type="file" ref="uploadFileInputRef" @change="handleUploadFile" accept=".txt,.docx,.pptx,.pdf,text/plain,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation" style="display:none">
|
||||
<input type="file" ref="uploadFileInputRef" @change="handleUploadFile" accept=".txt,.json,.toml,.yaml,.yml,.docx,.pptx,.pdf,text/plain,application/json,text/yaml,text/x-yaml,application/x-yaml,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation" multiple style="display:none">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -132,9 +132,34 @@
|
||||
<span class="btn-tooltip">{{ aiButtonLabel }}</span>
|
||||
</button>
|
||||
|
||||
<div class="size-indicator" :class="{ 'over-limit': isOverLimit }" aria-live="polite">
|
||||
{{ sizeInKB }} KB
|
||||
</div>
|
||||
<div
|
||||
class="size-indicator"
|
||||
:class="{ 'over-limit': isOverLimit }"
|
||||
@mouseenter="showSizeTooltip = true"
|
||||
@mouseleave="showSizeTooltip = false"
|
||||
>
|
||||
<svg
|
||||
class="warning-icon"
|
||||
:class="{ 'warning-icon--visible': isOverLimit }"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
{{ sizeInKB }} KB
|
||||
<Transition name="tooltip-fade">
|
||||
<div v-if="showSizeTooltip && isOverLimit" class="size-tooltip">
|
||||
<strong>文档超过32KB限制</strong>
|
||||
<span>AI补全功能已暂停,建议精简内容或分段处理</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false">
|
||||
@@ -153,6 +178,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadProgress" class="upload-progress-overlay">
|
||||
<div class="upload-progress-dialog">
|
||||
<div class="spinner"></div>
|
||||
<p>{{ t('uploading') || '正在上传文件' }}</p>
|
||||
<p class="progress-text">
|
||||
{{ uploadProgress.current }} / {{ uploadProgress.total }}
|
||||
</p>
|
||||
<p class="filename">{{ uploadProgress.filename }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -187,10 +223,12 @@ const contentSize = ref(0)
|
||||
const showImageDropdown = ref(false)
|
||||
const showExportDropdown = ref(false)
|
||||
const showUrlDialog = ref(false)
|
||||
const showSizeTooltip = ref(false)
|
||||
const imageUrl = ref('')
|
||||
const canUndo = ref(false)
|
||||
const canRedo = ref(false)
|
||||
const isDocUploadDisabled = ref(false)
|
||||
const uploadProgress = ref(null)
|
||||
const isOverLimit = computed(() => contentSize.value > SIZE_LIMIT)
|
||||
const sizeInKB = computed(() => Math.floor(contentSize.value / 1024))
|
||||
const undoLabel = computed(() => t('undo') || 'Undo')
|
||||
@@ -220,8 +258,8 @@ const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||
const MARKDOWN_EXT_RE = /\.md$/i
|
||||
const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif|avif)$/i
|
||||
const CONVERT_EXT_RE = /\.(docx|pptx|pdf)$/i
|
||||
const TEXT_EXT_RE = /\.txt$/i
|
||||
const TEXT_MIME_TYPES = new Set(['text/plain'])
|
||||
const TEXT_EXT_RE = /\.(txt|json|toml|ya?ml)$/i
|
||||
const TEXT_MIME_TYPES = new Set(['text/plain', 'application/json', 'text/yaml', 'text/x-yaml', 'application/x-yaml'])
|
||||
const CONVERT_MIME_TYPES = new Set([
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
@@ -905,8 +943,63 @@ const insertDocBlockAtCursor = (attrs) => {
|
||||
const nextPos = Math.min(from + blockNode.nodeSize, tr.doc.content.size)
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(nextPos), 1))
|
||||
view.dispatch(tr.scrollIntoView())
|
||||
view.focus()
|
||||
view.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const insertEmptyParagraph = () => {
|
||||
if (!crepe) return
|
||||
crepe.editor.action((ctx) => {
|
||||
const view = ctx.get(editorViewCtx)
|
||||
const { state } = view
|
||||
const { from, to } = state.selection
|
||||
const tr = state.tr.insertText('\n\n', from, to)
|
||||
const nextPos = from + 2
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(nextPos), 1))
|
||||
view.dispatch(tr)
|
||||
})
|
||||
}
|
||||
|
||||
const insertMultipleDocBlocks = (blocks) => {
|
||||
if (!crepe || blocks.length === 0) return
|
||||
|
||||
crepe.editor.action((ctx) => {
|
||||
const view = ctx.get(editorViewCtx)
|
||||
let tr = view.state.tr
|
||||
const docBlockType = view.state.schema.nodes[DOC_BLOCK_NODE_TYPE]
|
||||
if (!docBlockType) return
|
||||
|
||||
let currentPos = tr.selection.from
|
||||
|
||||
blocks.forEach((block, index) => {
|
||||
const maxPos = tr.doc.content.size
|
||||
|
||||
if (index > 0) {
|
||||
const insertPos = Math.min(currentPos, maxPos)
|
||||
tr = tr.insertText('\n', insertPos, insertPos)
|
||||
currentPos = insertPos + 1
|
||||
}
|
||||
|
||||
const blockNode = docBlockType.create({
|
||||
docType: block.docType,
|
||||
docName: block.docName,
|
||||
uploadTime: block.uploadTime,
|
||||
content: block.content,
|
||||
collapsed: Boolean(block.collapsed),
|
||||
})
|
||||
|
||||
const insertBlockPos = Math.min(currentPos, tr.doc.content.size)
|
||||
tr = tr.replaceRangeWith(insertBlockPos, insertBlockPos, blockNode)
|
||||
currentPos = insertBlockPos + blockNode.nodeSize
|
||||
})
|
||||
|
||||
const finalPos = Math.min(currentPos, tr.doc.content.size)
|
||||
if (finalPos >= 0 && finalPos <= tr.doc.content.size) {
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(finalPos), 1))
|
||||
}
|
||||
view.dispatch(tr.scrollIntoView())
|
||||
view.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const triggerFileUpload = () => {
|
||||
@@ -915,51 +1008,101 @@ const triggerFileUpload = () => {
|
||||
}
|
||||
|
||||
const handleUploadFile = async (event) => {
|
||||
const input = event.target
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
const input = event.target
|
||||
const files = Array.from(input.files || [])
|
||||
if (files.length === 0) return
|
||||
|
||||
try {
|
||||
if (!isSupportedDocFile(file)) {
|
||||
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
||||
return
|
||||
}
|
||||
const BATCH_LIMIT = 10
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||
|
||||
if (isDocUploadDisabled.value || !crepe) {
|
||||
alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件')
|
||||
return
|
||||
}
|
||||
if (files.length > BATCH_LIMIT) {
|
||||
alert(t('uploadBatchLimit') || `一次最多上传${BATCH_LIMIT}个文件`)
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const docType = getDocTypeFromFilename(file.name)
|
||||
let content = ''
|
||||
|
||||
if (isTextFile(file)) {
|
||||
content = await file.text()
|
||||
} else if (isConvertibleFile(file)) {
|
||||
content = await convertFileToMarkdown(file)
|
||||
} else {
|
||||
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
||||
return
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw new Error('文档解析结果为空')
|
||||
}
|
||||
|
||||
clearCurrentGhost()
|
||||
insertDocBlockAtCursor({
|
||||
docType,
|
||||
docName: file.name || `document.${docType}`,
|
||||
uploadTime: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
content,
|
||||
})
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : ''
|
||||
warnConvertError(message)
|
||||
} finally {
|
||||
input.value = ''
|
||||
for (const file of files) {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
alert(t('uploadSizeLimit') || `${file.name} 超过${MAX_FILE_SIZE / 1024 / 1024}MB限制`)
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (!isSupportedDocFile(file)) {
|
||||
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isDocUploadDisabled.value || !crepe) {
|
||||
alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const total = files.length
|
||||
uploadProgress.value = { current: 0, total, filename: '' }
|
||||
|
||||
const results = []
|
||||
const errors = []
|
||||
|
||||
for (let index = 0; index < files.length; index++) {
|
||||
const file = files[index]
|
||||
uploadProgress.value = { current: index + 1, total, filename: file.name }
|
||||
|
||||
try {
|
||||
const docType = getDocTypeFromFilename(file.name)
|
||||
let content = ''
|
||||
|
||||
if (isTextFile(file)) {
|
||||
content = await file.text()
|
||||
} else if (isConvertibleFile(file)) {
|
||||
content = await convertFileToMarkdown(file)
|
||||
} else {
|
||||
throw new Error('不支持的文件类型')
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw new Error('文档解析结果为空')
|
||||
}
|
||||
|
||||
results.push({
|
||||
docType,
|
||||
docName: file.name || `document.${docType}`,
|
||||
content,
|
||||
index,
|
||||
})
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : ''
|
||||
errors.push({ filename: file.name, message })
|
||||
}
|
||||
}
|
||||
|
||||
uploadProgress.value = null
|
||||
clearCurrentGhost()
|
||||
|
||||
results.sort((a, b) => a.index - b.index)
|
||||
|
||||
const blocksToInsert = results.map(({ docType, docName, content }) => ({
|
||||
docType,
|
||||
docName,
|
||||
content,
|
||||
uploadTime: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
}))
|
||||
|
||||
insertMultipleDocBlocks(blocksToInsert)
|
||||
|
||||
if (errors.length > 0) {
|
||||
const failCount = errors.length
|
||||
const errorMsgs = errors.map(e => `${e.filename}: ${e.message}`).join('\n')
|
||||
alert(`上传失败 ${failCount} 个文件:\n\n${errorMsgs}`)
|
||||
}
|
||||
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const handleImageUpload = async (event) => {
|
||||
@@ -1127,14 +1270,81 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.size-indicator {
|
||||
font-size: 10px;
|
||||
color: var(--muted-text);
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 10px;
|
||||
color: var(--muted-text);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.size-indicator.over-limit {
|
||||
color: var(--danger-text);
|
||||
color: var(--danger-text);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
animation: pulse-warning 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.warning-icon--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes pulse-warning {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.75;
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.size-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--tooltip-bg);
|
||||
color: var(--tooltip-fg);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
box-shadow: var(--panel-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.size-tooltip strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.size-tooltip span {
|
||||
opacity: 0.85;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tooltip-fade-enter-active,
|
||||
.tooltip-fade-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tooltip-fade-enter-from,
|
||||
.tooltip-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
@@ -1451,7 +1661,56 @@ onUnmounted(() => {
|
||||
|
||||
.copilot-ghost-block pre,
|
||||
.copilot-ghost-block code {
|
||||
background-color: var(--ghost-code-bg);
|
||||
background-color: var(--ghost-code-bg);
|
||||
}
|
||||
|
||||
.upload-progress-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.upload-progress-dialog {
|
||||
background: var(--editor-bg, white);
|
||||
padding: 24px 32px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto 16px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.filename {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -743,7 +743,12 @@ export function interruptCopilot(view: EditorView): void {
|
||||
}
|
||||
|
||||
export function checkSizeLimit(view: EditorView): { size: number; overLimit: boolean } {
|
||||
const size = view.state.doc.content.size
|
||||
let size = view.state.doc.content.size
|
||||
view.state.doc.descendants((node) => {
|
||||
if (node.type.name === 'doc_block' && node.attrs.content) {
|
||||
size += String(node.attrs.content).length
|
||||
}
|
||||
})
|
||||
return { size, overLimit: size > SIZE_LIMIT }
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ const HEADER_SEPARATOR = '\n---\n'
|
||||
export function normalizeDocType(value = '') {
|
||||
const lower = String(value || '').trim().toLowerCase()
|
||||
if (lower === 'txt' || lower === 'text' || lower === 'plain') return 'txt'
|
||||
if (lower === 'json') return 'json'
|
||||
if (lower === 'toml') return 'toml'
|
||||
if (lower === 'yaml' || lower === 'yml') return 'yaml'
|
||||
if (lower === 'doc' || lower === 'docx' || lower === 'word') return 'docx'
|
||||
if (lower === 'ppt' || lower === 'pptx' || lower === 'powerpoint') return 'pptx'
|
||||
if (lower === 'pdf') return 'pdf'
|
||||
@@ -20,6 +23,9 @@ export function getDocTypeFromFilename(name = '') {
|
||||
if (lower.endsWith('.docx')) return 'docx'
|
||||
if (lower.endsWith('.pptx')) return 'pptx'
|
||||
if (lower.endsWith('.pdf')) return 'pdf'
|
||||
if (lower.endsWith('.json')) return 'json'
|
||||
if (lower.endsWith('.toml')) return 'toml'
|
||||
if (lower.endsWith('.yaml') || lower.endsWith('.yml')) return 'yaml'
|
||||
return 'txt'
|
||||
}
|
||||
|
||||
@@ -29,10 +35,18 @@ export function isSupportedDocFile(file) {
|
||||
const type = String(file.type || '').toLowerCase()
|
||||
return (
|
||||
name.endsWith('.txt') ||
|
||||
name.endsWith('.json') ||
|
||||
name.endsWith('.toml') ||
|
||||
name.endsWith('.yaml') ||
|
||||
name.endsWith('.yml') ||
|
||||
name.endsWith('.docx') ||
|
||||
name.endsWith('.pptx') ||
|
||||
name.endsWith('.pdf') ||
|
||||
type === 'text/plain' ||
|
||||
type === 'application/json' ||
|
||||
type === 'text/yaml' ||
|
||||
type === 'text/x-yaml' ||
|
||||
type === 'application/x-yaml' ||
|
||||
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
|
||||
type === 'application/pdf'
|
||||
|
||||
+20
-11
@@ -36,15 +36,18 @@ export const translations = {
|
||||
uploadImg: 'Upload Image',
|
||||
uploadFile: 'Upload File',
|
||||
uploadDoc: 'Upload Document',
|
||||
uploadDocTypeWarning: 'Only txt, docx, pptx, pdf formats are supported.',
|
||||
uploadDocTypeWarning: 'Only txt, json, toml, yaml, docx, pptx, pdf formats are supported.',
|
||||
uploadDocSizeWarning: 'File size cannot exceed 10MB.',
|
||||
uploadDocInBlockWarning: 'Cannot insert document inside an existing document block. Please move cursor outside.',
|
||||
uploadDocError: 'Document conversion failed:',
|
||||
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
||||
uploadMdTypeWarning: 'Only Markdown (.md) files and image files are supported.',
|
||||
uploadFileError: 'File upload failed.',
|
||||
uploadConvertError: 'File conversion failed.',
|
||||
enableAI: 'Enable AI',
|
||||
uploadFileError: 'File upload failed.',
|
||||
uploadConvertError: 'File conversion failed.',
|
||||
uploadBatchLimit: 'Maximum 10 files at once',
|
||||
uploadSizeLimit: 'File exceeds 50MB limit',
|
||||
uploading: 'Uploading files...',
|
||||
enableAI: 'Enable AI',
|
||||
disableAI: 'Disable AI',
|
||||
insertUrl: 'Insert Image from URL',
|
||||
insert: 'Insert',
|
||||
@@ -90,15 +93,18 @@ export const translations = {
|
||||
uploadImg: '上传图片',
|
||||
uploadFile: '上传文件',
|
||||
uploadDoc: '上传文档',
|
||||
uploadDocTypeWarning: '仅支持 txt、docx、pptx、pdf 格式的文档',
|
||||
uploadDocTypeWarning: '仅支持 txt、json、toml、yaml、docx、pptx、pdf 格式的文档',
|
||||
uploadDocSizeWarning: '文件大小不能超过 10MB',
|
||||
uploadDocInBlockWarning: '无法在现有文档块内插入新文档,请将光标移到文档外部',
|
||||
uploadDocError: '文档转换失败:',
|
||||
uploadFileTypeWarning: '不支持的文件类型。仅支持 doc/docx/ppt/pptx/pdf/zip、图片、txt/json。',
|
||||
uploadMdTypeWarning: '仅支持 Markdown(.md)和图片文件。',
|
||||
uploadFileError: '文件上传失败',
|
||||
uploadConvertError: '文件转换失败',
|
||||
enableAI: '启用 AI',
|
||||
uploadFileError: '文件上传失败',
|
||||
uploadConvertError: '文件转换失败',
|
||||
uploadBatchLimit: '一次最多上传10个文件',
|
||||
uploadSizeLimit: '文件超过50MB限制',
|
||||
uploading: '正在上传文件...',
|
||||
enableAI: '启用 AI',
|
||||
disableAI: '禁用 AI',
|
||||
insertUrl: '通过 URL 插入图片',
|
||||
insert: '插入',
|
||||
@@ -145,9 +151,12 @@ export const translations = {
|
||||
uploadFile: 'Upload File',
|
||||
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
||||
uploadMdTypeWarning: 'Only Markdown (.md) files and image files are supported.',
|
||||
uploadFileError: 'File upload failed.',
|
||||
uploadConvertError: 'File conversion failed.',
|
||||
enableAI: 'AIを有効化',
|
||||
uploadFileError: 'File upload failed.',
|
||||
uploadConvertError: 'File conversion failed.',
|
||||
uploadBatchLimit: 'Maximum 10 files at once',
|
||||
uploadSizeLimit: 'File exceeds 50MB limit',
|
||||
uploading: 'Uploading files...',
|
||||
enableAI: 'AIを有効化',
|
||||
disableAI: 'AIを無効化',
|
||||
insertUrl: 'URLから画像を挿入',
|
||||
insert: '挿入',
|
||||
|
||||
Reference in New Issue
Block a user