281 lines
9.0 KiB
Svelte
281 lines
9.0 KiB
Svelte
<script lang="ts">
|
|
import { Modal } from 'flowbite-svelte';
|
|
import {
|
|
Check,
|
|
CircleAlert,
|
|
FileText,
|
|
RefreshCw,
|
|
Upload,
|
|
X
|
|
} from '@lucide/svelte';
|
|
import {
|
|
PARSERS,
|
|
type AeJournalEntryInput
|
|
} from '$lib/ae_journals/ae_journals_parsers';
|
|
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
|
import { ae_api } from '$lib/stores/ae_stores';
|
|
import { journals_slct } from '$lib/ae_journals/ae_journals_stores';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
on_close: () => void;
|
|
on_import_complete: () => void;
|
|
}
|
|
|
|
let { open = $bindable(false), on_close, on_import_complete }: Props = $props();
|
|
|
|
let files: FileList | null = $state(null);
|
|
let selected_parser: keyof typeof PARSERS = $state('standard');
|
|
let parsed_entries: AeJournalEntryInput[] = $state([]);
|
|
let is_parsing = $state(false);
|
|
let is_importing = $state(false);
|
|
let import_log: string[] = $state([]);
|
|
let is_dragging = $state(false);
|
|
|
|
// Watch for file selection or parser change to trigger parsing
|
|
$effect(() => {
|
|
if (files && files.length > 0) {
|
|
parse_files();
|
|
}
|
|
});
|
|
|
|
function handle_drag_enter(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
is_dragging = true;
|
|
}
|
|
|
|
function handle_drag_leave(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
is_dragging = false;
|
|
}
|
|
|
|
function handle_drag_over(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
is_dragging = true;
|
|
}
|
|
|
|
function handle_drop(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
is_dragging = false;
|
|
|
|
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
|
files = e.dataTransfer.files;
|
|
}
|
|
}
|
|
|
|
async function parse_files() {
|
|
if (!files) return;
|
|
is_parsing = true;
|
|
parsed_entries = [];
|
|
|
|
const parser = PARSERS[selected_parser];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
try {
|
|
const text = await file.text();
|
|
const entries = await parser(file, text);
|
|
parsed_entries = [...parsed_entries, ...entries];
|
|
} catch (err) {
|
|
console.error(`Error parsing ${file.name}:`, err);
|
|
}
|
|
}
|
|
is_parsing = false;
|
|
}
|
|
|
|
async function handle_import() {
|
|
if (parsed_entries.length === 0) return;
|
|
is_importing = true;
|
|
import_log = [];
|
|
|
|
const journal_id = $journals_slct.journal_id;
|
|
if (!journal_id) {
|
|
alert(
|
|
'No target journal selected. Please select a journal in the background first.'
|
|
);
|
|
is_importing = false;
|
|
return;
|
|
}
|
|
|
|
let success_count = 0;
|
|
|
|
for (const entry of parsed_entries) {
|
|
try {
|
|
// Construct payload
|
|
const data_kv = {
|
|
name: entry.name,
|
|
content: entry.content,
|
|
tags: entry.tags.join(', '),
|
|
type_code: entry.type_code || 'note',
|
|
created_on: entry.created_on,
|
|
updated_on: entry.updated_on
|
|
};
|
|
|
|
const res = await journals_func.create_ae_obj__journal_entry({
|
|
api_cfg: $ae_api,
|
|
journal_id: journal_id,
|
|
data_kv: data_kv,
|
|
log_lvl: 0
|
|
});
|
|
|
|
if (res) {
|
|
import_log.push(`✅ Imported: ${entry.name}`);
|
|
success_count++;
|
|
} else {
|
|
import_log.push(`❌ Failed: ${entry.name}`);
|
|
}
|
|
} catch (err) {
|
|
import_log.push(`❌ Error: ${entry.name} - ${err}`);
|
|
}
|
|
}
|
|
|
|
is_importing = false;
|
|
alert(
|
|
`Import complete! ${success_count}/${parsed_entries.length} imported.`
|
|
);
|
|
on_import_complete();
|
|
open = false;
|
|
}
|
|
</script>
|
|
|
|
<Modal
|
|
title="Import Journal Entries"
|
|
bind:open
|
|
autoclose={false}
|
|
size="xl"
|
|
class="w-full">
|
|
<div class="space-y-4">
|
|
<!-- Configuration -->
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div>
|
|
<label class="label">
|
|
<span>Parser Strategy</span>
|
|
<select class="select" bind:value={selected_parser}>
|
|
<option value="standard"
|
|
>Standard (1 File = 1 Entry)</option>
|
|
<option value="personal_log"
|
|
>Personal Log (Split by Date)</option>
|
|
<option value="amazon_vine">Amazon Vine Reviews</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<div class="label mb-2">
|
|
<span>Select Files</span>
|
|
</div>
|
|
|
|
<!-- Drop Zone -->
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="
|
|
flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2
|
|
border-dashed p-8 text-center transition-all duration-200
|
|
{is_dragging
|
|
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
|
: 'border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500'}
|
|
"
|
|
ondragenter={handle_drag_enter}
|
|
ondragleave={handle_drag_leave}
|
|
ondragover={handle_drag_over}
|
|
ondrop={handle_drop}
|
|
onclick={() =>
|
|
document.getElementById('file_import_input')?.click()}>
|
|
<Upload class="h-10 w-10 text-gray-400" />
|
|
<p class="text-sm text-gray-500">
|
|
<span
|
|
class="text-primary-600 hover:text-primary-500 font-semibold"
|
|
>Click to upload</span>
|
|
or drag and drop
|
|
</p>
|
|
<p class="text-xs text-gray-400">
|
|
Markdown (.md) or Text (.txt) files
|
|
</p>
|
|
|
|
<input
|
|
id="file_import_input"
|
|
type="file"
|
|
class="hidden"
|
|
multiple
|
|
accept=".md,.txt"
|
|
onchange={(e) => (files = e.currentTarget.files)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview -->
|
|
<div
|
|
class="max-h-64 overflow-y-auto rounded-lg border bg-gray-50 p-2 dark:bg-gray-900">
|
|
<h4 class="mb-2 flex justify-between font-bold">
|
|
<span>Preview ({parsed_entries.length} entries)</span>
|
|
{#if is_parsing}
|
|
<RefreshCw class="animate-spin" />
|
|
{/if}
|
|
</h4>
|
|
|
|
{#if parsed_entries.length > 0}
|
|
<table class="table-compact table w-full text-xs">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Date</th>
|
|
<th>Tags</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each parsed_entries as entry, i (i)}
|
|
<tr>
|
|
<td
|
|
class="max-w-[200px] truncate"
|
|
title={entry.name}>{entry.name}</td>
|
|
<td>{entry.created_on?.substring(0, 10)}</td>
|
|
<td>{entry.tags.join(', ')}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{:else if files && files.length > 0 && !is_parsing}
|
|
<div class="py-4 text-center text-gray-500">
|
|
No entries found in selected files.
|
|
</div>
|
|
{:else if !files}
|
|
<div class="py-4 text-center text-gray-500">
|
|
Select files to preview import.
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Import Log -->
|
|
{#if import_log.length > 0}
|
|
<div
|
|
class="max-h-32 overflow-y-auto rounded bg-black p-2 font-mono text-xs text-green-400">
|
|
{#each import_log as log, i (i)}
|
|
<div>{log}</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="modal-action">
|
|
<button
|
|
type="button"
|
|
class="btn preset-tonal-secondary"
|
|
onclick={on_close}>Cancel</button>
|
|
<button
|
|
type="button"
|
|
class="btn preset-filled-primary"
|
|
disabled={parsed_entries.length === 0 || is_importing}
|
|
onclick={handle_import}>
|
|
{#if is_importing}
|
|
Importing...
|
|
{:else}
|
|
Import {parsed_entries.length} Entries
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|