|
@@ -1,12 +1,15 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, computed } from "vue";
|
|
import { ref, computed } from "vue";
|
|
|
|
|
+import { ingestFile, ingestDocuments, ingestUrl } from "../api/chat.js";
|
|
|
|
|
|
|
|
const emit = defineEmits(["ingested"]);
|
|
const emit = defineEmits(["ingested"]);
|
|
|
|
|
|
|
|
const tab = ref("arquivo");
|
|
const tab = ref("arquivo");
|
|
|
const manualText = ref("");
|
|
const manualText = ref("");
|
|
|
const file = ref(null);
|
|
const file = ref(null);
|
|
|
|
|
+const urlInput = ref("");
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
|
|
+const uploadProgress = ref(0);
|
|
|
const status = ref("");
|
|
const status = ref("");
|
|
|
const error = ref("");
|
|
const error = ref("");
|
|
|
const dragOver = ref(false);
|
|
const dragOver = ref(false);
|
|
@@ -55,17 +58,31 @@ async function onIngest() {
|
|
|
error.value = "";
|
|
error.value = "";
|
|
|
status.value = "";
|
|
status.value = "";
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
|
|
+ uploadProgress.value = 0;
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
- if (tab.value === "texto" && manualText.value.trim()) {
|
|
|
|
|
- emit("ingested", { text: manualText.value.trim(), source: "manual" });
|
|
|
|
|
- } else if (file.value) {
|
|
|
|
|
- emit("ingested", { file: file.value, source: "upload" });
|
|
|
|
|
|
|
+ let result;
|
|
|
|
|
+
|
|
|
|
|
+ if (tab.value === "texto") {
|
|
|
|
|
+ result = await ingestDocuments([{ text: manualText.value.trim(), source: "manual" }]);
|
|
|
|
|
+ manualText.value = "";
|
|
|
|
|
+ } else if (tab.value === "arquivo") {
|
|
|
|
|
+ result = await ingestFile(file.value, {
|
|
|
|
|
+ source: file.value.name,
|
|
|
|
|
+ onProgress: (pct) => { uploadProgress.value = pct; }
|
|
|
|
|
+ });
|
|
|
|
|
+ file.value = null;
|
|
|
|
|
+ if (fileInputRef.value) fileInputRef.value.value = "";
|
|
|
|
|
+ } else if (tab.value === "url") {
|
|
|
|
|
+ result = await ingestUrl(urlInput.value.trim());
|
|
|
|
|
+ urlInput.value = "";
|
|
|
}
|
|
}
|
|
|
- manualText.value = "";
|
|
|
|
|
- file.value = null;
|
|
|
|
|
- if (fileInputRef.value) fileInputRef.value.value = "";
|
|
|
|
|
- status.value = "Documento enviado para ingestão com sucesso.";
|
|
|
|
|
|
|
+
|
|
|
|
|
+ uploadProgress.value = 0;
|
|
|
|
|
+ status.value = `Ingerido com sucesso${result?.upserted != null ? ` (${result.upserted} chunks)` : ""}.`;
|
|
|
|
|
+ emit("ingested", result);
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
|
|
+ uploadProgress.value = 0;
|
|
|
error.value = e?.message || "Erro desconhecido.";
|
|
error.value = e?.message || "Erro desconhecido.";
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
@@ -74,6 +91,7 @@ async function onIngest() {
|
|
|
|
|
|
|
|
const canSubmit = computed(() => {
|
|
const canSubmit = computed(() => {
|
|
|
if (tab.value === "arquivo") return !!file.value;
|
|
if (tab.value === "arquivo") return !!file.value;
|
|
|
|
|
+ if (tab.value === "url") return urlInput.value.trim().startsWith("https://");
|
|
|
return !!manualText.value.trim();
|
|
return !!manualText.value.trim();
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
@@ -82,7 +100,7 @@ const canSubmit = computed(() => {
|
|
|
<div class="grid gap-4">
|
|
<div class="grid gap-4">
|
|
|
<div class="flex gap-1 rounded-xl bg-gray-100 p-1 dark:bg-white/5">
|
|
<div class="flex gap-1 rounded-xl bg-gray-100 p-1 dark:bg-white/5">
|
|
|
<button
|
|
<button
|
|
|
- v-for="t in [{ key: 'arquivo', label: 'Arquivo' }, { key: 'texto', label: 'Texto' }]"
|
|
|
|
|
|
|
+ v-for="t in [{ key: 'arquivo', label: 'Arquivo' }, { key: 'url', label: 'URL' }, { key: 'texto', label: 'Texto' }]"
|
|
|
:key="t.key"
|
|
:key="t.key"
|
|
|
type="button"
|
|
type="button"
|
|
|
class="flex-1 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors"
|
|
class="flex-1 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors"
|
|
@@ -95,9 +113,8 @@ const canSubmit = computed(() => {
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
+ <!-- Aba: Arquivo -->
|
|
|
<template v-if="tab === 'arquivo'">
|
|
<template v-if="tab === 'arquivo'">
|
|
|
-
|
|
|
|
|
<div
|
|
<div
|
|
|
v-if="file"
|
|
v-if="file"
|
|
|
class="flex items-center gap-3 rounded-xl border border-gray-200 bg-background/60 p-3 dark:border-gray-700 dark:bg-background/10"
|
|
class="flex items-center gap-3 rounded-xl border border-gray-200 bg-background/60 p-3 dark:border-gray-700 dark:bg-background/10"
|
|
@@ -128,7 +145,6 @@ const canSubmit = computed(() => {
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
-
|
|
|
|
|
<div
|
|
<div
|
|
|
v-else
|
|
v-else
|
|
|
class="relative flex cursor-pointer flex-col items-center gap-3 rounded-xl border-2 border-dashed px-6 py-10 transition-colors"
|
|
class="relative flex cursor-pointer flex-col items-center gap-3 rounded-xl border-2 border-dashed px-6 py-10 transition-colors"
|
|
@@ -149,7 +165,7 @@ const canSubmit = computed(() => {
|
|
|
<div class="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
<div class="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
|
Arraste um arquivo ou <span class="text-primary underline underline-offset-2">clique para selecionar</span>
|
|
Arraste um arquivo ou <span class="text-primary underline underline-offset-2">clique para selecionar</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">PDF, Word, TXT, PNG, JPG, WEBP</div>
|
|
|
|
|
|
|
+ <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">PDF, Word, TXT, PNG, JPG, WEBP — até 25 MB</div>
|
|
|
</div>
|
|
</div>
|
|
|
<input
|
|
<input
|
|
|
ref="fileInputRef"
|
|
ref="fileInputRef"
|
|
@@ -159,9 +175,32 @@ const canSubmit = computed(() => {
|
|
|
@change="onFileInputChange"
|
|
@change="onFileInputChange"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Barra de progresso -->
|
|
|
|
|
+ <div v-if="uploadProgress > 0" class="overflow-hidden rounded-full bg-gray-100 dark:bg-white/10">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="h-1.5 rounded-full bg-primary transition-all duration-200"
|
|
|
|
|
+ :style="{ width: `${uploadProgress}%` }"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
+ <!-- Aba: URL -->
|
|
|
|
|
+ <template v-else-if="tab === 'url'">
|
|
|
|
|
+ <div class="grid gap-2">
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="urlInput"
|
|
|
|
|
+ type="url"
|
|
|
|
|
+ placeholder="https://exemplo.com/pagina"
|
|
|
|
|
+ class="w-full rounded-xl border border-gray-200 bg-background px-3 py-2.5 text-sm text-gray-700 placeholder-gray-400 outline-none focus:border-primary dark:border-gray-700 dark:bg-white/5 dark:text-gray-200 dark:placeholder-gray-500"
|
|
|
|
|
+ />
|
|
|
|
|
+ <p class="text-xs text-gray-400 dark:text-gray-500">
|
|
|
|
|
+ Apenas URLs <code class="font-mono">https://</code> públicas. O conteúdo da página será extraído e indexado.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Aba: Texto -->
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
<div class="rounded-2xl border border-gray-200 bg-white/70 p-4 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-white/[0.04]">
|
|
<div class="rounded-2xl border border-gray-200 bg-white/70 p-4 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-white/[0.04]">
|
|
|
<textarea
|
|
<textarea
|
|
@@ -173,12 +212,11 @@ const canSubmit = computed(() => {
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
-
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|
|
- {{ tab === 'arquivo'
|
|
|
|
|
- ? 'O arquivo será processado e indexado na base de conhecimento.'
|
|
|
|
|
- : 'O texto será fragmentado e indexado na base.' }}
|
|
|
|
|
|
|
+ <span v-if="tab === 'arquivo'">O arquivo será processado e indexado na base de conhecimento.</span>
|
|
|
|
|
+ <span v-else-if="tab === 'url'">A página será baixada, o texto extraído e indexado.</span>
|
|
|
|
|
+ <span v-else>O texto será fragmentado e indexado na base.</span>
|
|
|
</div>
|
|
</div>
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
@@ -190,11 +228,11 @@ const canSubmit = computed(() => {
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
|
|
</svg>
|
|
</svg>
|
|
|
- {{ loading ? "Enviando..." : "Ingerir" }}
|
|
|
|
|
|
|
+ <span v-if="loading && tab === 'arquivo' && uploadProgress > 0">{{ uploadProgress }}%</span>
|
|
|
|
|
+ <span v-else>{{ loading ? "Processando..." : "Ingerir" }}</span>
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
-
|
|
|
|
|
<div
|
|
<div
|
|
|
v-if="status"
|
|
v-if="status"
|
|
|
class="rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-300"
|
|
class="rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-300"
|