7 Commits

Author SHA1 Message Date
Scott Idem
1ef9080cda Hide journal entry AI tools unless admin edit mode is active
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:24:10 -04:00
Scott Idem
66c0be65c4 Show AI tools only in edit mode; open existing summary without regenerating
- Hide AI tools panel when entry is not in edit mode
- Clicking AI button when a summary already exists opens it directly
  instead of triggering a new API call; Re-run still available in modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:22:16 -04:00
Scott Idem
bdba092de0 Polish journal entry save buttons and header controls
- Add hover titles to all save buttons
- Match warning color scheme across floating, inline, and header save buttons
- Fix floating save button visibility (Tailwind v4 hidden/md:inline-flex conflict)
- Hide floating save when no unsaved changes using {#if}
- Hide Config button when not in admin edit mode
- Remove the mobile/backup explicit Save button from header (redundant)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:15:52 -04:00
Scott Idem
0fa93d7ee5 Fix auto-save stopping after the first save in journal entry editor
The auto-save $effect wrapped has_unsaved_changes in untrack(), which meant it
was never tracked when save_status was 'saved' (JS short-circuit in the else-if
branch). After every successful save the effect lost its reactive dependency on
user edits and never re-fired until something else changed save_status first.

Fix: track tmp_entry_obj.content and tmp_entry_obj.name directly (void reads)
so the effect re-runs on every keystroke and the debounce timer resets correctly
(fires 2 s after the last change, not the first). has_unsaved_changes is also
tracked directly so the status indicator resets cleanly when changes are cleared.
All side-effects remain in untrack() to prevent reactive loops.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 16:21:22 -04:00
Scott Idem
847d653b5e Truncate journal and entry names in browser tab titles
Entry: 50 chars for entry name, 30 for journal name
Journal: 60 chars for journal name
Appends ellipsis (…) when truncated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 16:08:41 -04:00
Scott Idem
cd01a87143 Fix browser tab titles for journal and journal entry pages
Journal: [Journal Name] - OSIT's AE Journals
Entry:   [Entry Name] - [Journal Name] - OSIT's AE Journals

Entry page title was previously commented out; now active with correct format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 16:00:31 -04:00
Scott Idem
60ecd221b4 Add tab indentation and line number toggle to CodeMirror editor
- Wire indentWithTab into keymap (Tab=indent, Shift-Tab=dedent, 4 spaces)
- Set indentUnit to 4 spaces
- Wrap lineNumbers() in a Compartment for live toggle without editor rebuild
- Add Hash toolbar button to toggle line numbers; respects show_line_numbers prop as initial value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:55:42 -04:00
8 changed files with 155 additions and 69 deletions

View File

@@ -157,15 +157,23 @@ function handle_save() {
<!-- Trigger Button -->
<button
type="button"
onclick={generate_ai_result}
onclick={() => {
if (summary) {
tmp_summary = summary;
active_tab = 'result';
show_modal = true;
} else {
generate_ai_result();
}
}}
class={buttonClass}
title="Generate AI summary/analysis">
title={summary ? 'View existing AI summary' : 'Generate AI summary/analysis'}>
{#await ae_promises}
<Loader class="mr-1 inline-block animate-spin" size="1.2em" />
<span class="text-sm">Processing...</span>
{:then}
<BotMessageSquare class="mr-1 inline-block" size="1.2em" />
<span class="text-sm">Summarize</span>
<span class="text-sm hidden">Summarize</span>
{:catch}
<span class="text-sm text-red-500">Error</span>
{/await}
@@ -181,6 +189,7 @@ function handle_save() {
class="btn btn-sm preset-tonal-surface shadow-md"
title="AI Settings">
<Settings size="1.2em" />
<span class="text-sm hidden">Settings</span>
</button>
<!-- Unified AI Modal -->

View File

@@ -81,6 +81,7 @@ type CMCache = {
languages?: any;
oneDark?: any;
placeholderExt?: any;
Compartment?: any;
} | null;
const GLOBAL_KEY = '__cm_singleton_modules_v1';
@@ -135,6 +136,7 @@ export async function ensure_CodeMirror_modules(): Promise<CMCache> {
EditorState_allowMultipleSelections:
stateMod.EditorState.allowMultipleSelections,
EditorState_readOnly: stateMod.EditorState.readOnly,
Compartment: stateMod.Compartment,
markdown: markdownMod?.markdown,
markdownLanguage: markdownMod?.markdownLanguage,

View File

@@ -11,7 +11,7 @@ import { ensure_CodeMirror_modules } from './codemirror_modules';
// import type { key_val } from '$lib/stores/ae_stores';
// Icons (Standardized to Lucide where possible, or FontAwesome placeholders)
import { Bold, Code, Italic, List } from '@lucide/svelte';
import { Bold, Code, Hash, Italic, List } from '@lucide/svelte';
interface Props {
content?: string | null;
new_content?: string;
@@ -42,6 +42,8 @@ let {
let editor_container: HTMLDivElement | undefined = $state();
let cm: any = $state(); // CodeMirror modules cache
let show_line_nums = $state(untrack(() => show_line_numbers));
let ln_compartment: any = null;
async function create_editor() {
if (!browser) return;
@@ -55,6 +57,8 @@ async function create_editor() {
editor_view = null;
}
ln_compartment = new cm.Compartment();
const extensions = [
cm.highlightSpecialChars(),
cm.history(),
@@ -70,8 +74,9 @@ async function create_editor() {
cm.highlightActiveLine(),
cm.highlightActiveLineGutter(),
// Keymaps
// Keymaps — indentWithTab must come before defaultKeymap
cm.keymap.of([
cm.indentWithTab,
...cm.defaultKeymap,
...cm.searchKeymap,
...cm.historyKeymap,
@@ -80,6 +85,9 @@ async function create_editor() {
...cm.lintKeymap
]),
// 4-space indentation unit
cm.indentUnit.of(' '),
// Language Support
language === 'markdown'
? cm.markdown({ base: cm.markdownLanguage })
@@ -94,7 +102,7 @@ async function create_editor() {
readonly
? cm.EditorState.readOnly.of(true)
: cm.EditorView.editable.of(true),
show_line_numbers ? cm.lineNumbers() : null,
ln_compartment.of(show_line_nums ? cm.lineNumbers() : []),
wrap_lines ? cm.EditorView_lineWrapping : null,
placeholder ? cm.placeholderExt(placeholder) : null,
@@ -144,6 +152,14 @@ $effect(() => {
}
});
// Toggle line numbers without rebuilding the editor
$effect(() => {
if (!editor_view || !ln_compartment || !cm) return;
editor_view.dispatch({
effects: ln_compartment.reconfigure(show_line_nums ? cm.lineNumbers() : [])
});
});
// *** Toolbar Helpers
const wrap_selection = (before: string, after: string = before) => {
if (!editor_view) return;
@@ -244,6 +260,15 @@ const toggle_list = () => {
</button>
<div class="ml-auto flex gap-1">
<button
type="button"
class="btn btn-sm {show_line_nums
? 'variant-filled-primary'
: 'variant-soft'} hover:variant-filled-primary"
onclick={() => (show_line_nums = !show_line_nums)}
title="Toggle Line Numbers">
<Hash size="14" />
</button>
<span
class="mr-2 self-center text-[10px] font-bold uppercase opacity-50"
>{language}</span>

View File

@@ -303,12 +303,18 @@ if (browser) {
}
import { LoaderCircle } from '@lucide/svelte';
const title_journal = $derived(
$lq__journal_obj?.name
? $lq__journal_obj.name.length > 60
? $lq__journal_obj.name.slice(0, 60) + '…'
: $lq__journal_obj.name
: 'Journal'
);
</script>
<svelte:head>
<title>
Æ Journals: {$lq__journal_obj?.name ?? ''} - {$ae_loc?.title}
</title>
<title>{title_journal} - OSIT's AE Journals</title>
</svelte:head>
{#if $lq__journal_obj === undefined}

View File

@@ -258,15 +258,26 @@ $effect(() => {
// log_lvl = 1;
}
});
const title_entry = $derived(
$lq__journal_entry_obj?.name
? $lq__journal_entry_obj.name.length > 50
? $lq__journal_entry_obj.name.slice(0, 50) + '…'
: $lq__journal_entry_obj.name
: 'Entry'
);
const title_journal = $derived(
$lq__journal_obj?.name
? $lq__journal_obj.name.length > 30
? $lq__journal_obj.name.slice(0, 30) + '…'
: $lq__journal_obj.name
: 'Journal'
);
</script>
<!-- <svelte:head>
<title>
&AElig; Journals:
{$lq__journal_entry_obj?.name ? ae_util.shorten_string({ string: $lq__journal_entry_obj?.name, max_length: 20, begin_length: 10, end_length: 4 }) : ''}
- {$ae_loc?.title}
</title>
</svelte:head> -->
<svelte:head>
<title>{title_entry} - {title_journal} - OSIT's AE Journals</title>
</svelte:head>
{#if $ae_loc.person_id == $lq__journal_obj?.person_id || $lq__journal_entry_obj?.public}
<section

View File

@@ -4,7 +4,9 @@
* Extracted 2026-01-08 to modularize the massive Journal Entry view.
* Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes.
*/
import { LockKeyhole, RefreshCcw, Save } from '@lucide/svelte';
import {
LockKeyhole, RefreshCcw, Save
} from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
import { journals_loc } from '$lib/ae_journals/ae_journals_stores';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
@@ -67,6 +69,7 @@ const preferred_viewer = $derived(
<div
class="prose dark:prose-invert w-full max-w-none overflow-x-auto rounded-lg border border-gray-200 bg-white p-4 shadow-lg dark:border-gray-700 dark:bg-gray-900">
<!-- The rendered HTML branch is intentionally trusted input from the journal editor pipeline. -->
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html tmp_entry_obj?.content_md_html || ''}
</div>
{/if}
@@ -120,21 +123,30 @@ const preferred_viewer = $derived(
placeholder="Edit content..."></textarea>
{/if}
<!-- Floating Save Button -->
<button
type="button"
onclick={on_save}
disabled={!has_changed}
class="btn btn-sm md:btn-md lg:btn-lg preset-filled-success fixed top-84 right-6 z-20 hidden min-w-32 shadow-xl transition-all md:inline-flex"
class:hidden={!has_changed}>
<Save size="1.2em" class="mr-2" /> Save
</button>
<!-- Floating Save Button (desktop only) -->
{#if has_changed}
<button
type="button"
onclick={on_save}
class="
btn btn-sm md:btn-md lg:btn-lg preset-tonal-warning hover:preset-filled-warning-500
fixed top-84 right-6 z-20
min-w-32 shadow-xl
max-sm:hidden
"
title="Save changes"
>
<Save size="1.2em" class="" />Save
</button>
{/if}
<!-- Inline Save Button (Mobile/Context) -->
<button
type="button"
onclick={on_save}
disabled={!has_changed}
title="Save changes"
class="btn preset-tonal-warning hover:preset-filled-warning-500 mt-4 w-full max-w-96"
class:invisible={!has_changed}>
<Save size="1.2em" class="mr-2" /> Save Changes

View File

@@ -9,7 +9,7 @@ import {
CircleCheck,
CircleX,
Eye,
Fingerprint,
// Fingerprint,
LoaderCircle,
LockKeyhole,
LockKeyholeOpen,
@@ -19,6 +19,7 @@ import {
Settings
} from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess
@@ -83,10 +84,13 @@ function toggle_edit_mode() {
<button
type="button"
onclick={toggle_edit_mode}
title={$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'
? (has_changed ? 'Save & exit edit mode' : 'Exit edit mode')
: 'Edit entry'}
class="btn-icon btn-icon-sm transition-colors duration-150 {has_changed &&
$journals_loc.entry.edit_kv[entry.journal_entry_id] ===
'current'
? 'preset-filled-success'
? 'preset-tonal-warning hover:preset-filled-warning-500'
: 'preset-tonal-surface'}">
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
{#if has_changed}<Save size="1.2em" />{:else}<Eye
@@ -160,24 +164,34 @@ function toggle_edit_mode() {
</button>
{/if}
<div class="bg-surface-500/20 mx-1 h-6 w-px"></div>
<!-- Unified Config Button -->
<button
type="button"
class="btn btn-sm preset-tonal-primary font-bold transition-colors duration-150"
onclick={on_show_config}>
<Settings size="1.1em" class="mr-2" /> Config
onclick={on_show_config}
class:hidden={!$ae_loc.edit_mode}
class="
btn btn-sm preset-tonal-primary font-bold transition-colors duration-150
"
title="Entry Configuration"
>
<Settings size="1.1em" />Config
</button>
<!-- Explicit Save (Mobile/Backup) -->
{#if has_changed && save_status !== 'saving'}
<!-- {#if has_changed && save_status !== 'saving'}
<button
type="button"
class="btn btn-sm preset-filled-primary transition-colors duration-150"
onclick={on_save}>
<Save size="1.1em" class="mr-2" /> Save
onclick={on_save}
class="
btn btn-sm
preset-filled-primary
transition-colors duration-150
md:hidden
"
title="Save changes"
>
<Save size="1.1em" class="" />Save
</button>
{/if}
{/if} -->
</div>
</header>

View File

@@ -154,33 +154,38 @@ $effect(() => {
// 2. Auto-Save Debounce
$effect(() => {
// Isolate logic from secondary dependencies
const should_save = untrack(
() => has_unsaved_changes && !is_processing && save_status !== 'saving'
);
// Track content and name directly so this effect re-runs on every keystroke,
// resetting the debounce timer each time (fires 2 s after the LAST change).
// Tracking has_unsaved_changes ensures the effect also wakes up when changes
// are cleared (e.g. after a save) so the status indicator resets correctly.
// All side-effects (save_status writes, $journals_loc reads) stay in untrack
// to avoid creating reactive loops.
void tmp_entry_obj.content;
void tmp_entry_obj.name;
const changed = has_unsaved_changes;
if (should_save) {
if (save_status !== 'saving') save_status = 'unsaved';
clearTimeout(auto_save_timer);
const auto_save_enabled = untrack(() => $journals_loc.entry.auto_save);
if (auto_save_enabled) {
clearTimeout(auto_save_timer);
auto_save_timer = setTimeout(() => {
if (
untrack(
() =>
has_unsaved_changes &&
!is_processing &&
save_status !== 'saving'
)
) {
update_journal_entry();
}
}, 2000);
}
} else if (save_status === 'unsaved' && !has_unsaved_changes) {
save_status = 'saved';
if (!changed) {
untrack(() => {
if (save_status === 'unsaved') save_status = 'saved';
});
return;
}
untrack(() => {
if (save_status !== 'saving') save_status = 'unsaved';
});
if (!untrack(() => $journals_loc.entry.auto_save)) return;
auto_save_timer = setTimeout(() => {
untrack(() => {
if (has_unsaved_changes && !is_processing && save_status !== 'saving') {
update_journal_entry();
}
});
}, 2000);
});
// 3. Auto-Decryption Workflow
@@ -482,13 +487,15 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
] == 'current'
? 'ring-primary-500/40 ring-2 ring-inset'
: ''}">
<AE_Comp_Journal_Entry_AiTools
content={typeof tmp_entry_obj.content === 'string'
? tmp_entry_obj.content
: ''}
bind:summary={tmp_entry_obj.summary}
on_save={() => update_journal_entry()}
{log_lvl} />
{#if $ae_loc.edit_mode && $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] === 'current'}
<AE_Comp_Journal_Entry_AiTools
content={typeof tmp_entry_obj.content === 'string'
? tmp_entry_obj.content
: ''}
bind:summary={tmp_entry_obj.summary}
on_save={() => update_journal_entry()}
{log_lvl} />
{/if}
<AE_Comp_Journal_Entry_Editor
entry={$lq__journal_entry_obj}