Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m55s
Forgejo Docker Build / Build Docker image (push) Successful in 9s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The ceiling was 100 MB per file with ten files allowed at once, and every file is held whole in memory to be parsed — so the old limit let a single request ask for a gigabyte of heap. A source article that size is not a thing anyone uploads here. Now 10 MB, defined once and used by both the multer limit and the post-upload check. The filter accepted `allowed mime OR allowed extension`, so naming a file .pdf was enough on its own, whatever it declared — and the extension is chosen by whoever uploads. Both are required now. Neither of those sees any bytes: multer filters on the headers, before the file has arrived. verifySources() runs once the buffer exists and refuses a file whose contents are not what its type claims, using the same helper as documents, S3 uploads and assistant attachments. It runs before extraction, because an extractor handed a malformed file is where the damage would happen. The CMS screen said 100 MB and listed four of the ten accepted formats; it now says what the server actually does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
327 lines
22 KiB
HTML
327 lines
22 KiB
HTML
<div class="module-header">
|
|
<h2><i class="fas fa-pen-to-square"></i> Content Manager</h2>
|
|
<p>Create and manage Learning Hub content, quizzes, and categories</p>
|
|
</div>
|
|
|
|
<!-- Stats bar -->
|
|
<div class="cms-stats-bar" id="lh-cms-stats-bar">
|
|
<div class="cms-stat"><span class="cms-stat-value" id="cms-stat-published">-</span><span class="cms-stat-label">Published</span></div>
|
|
<div class="cms-stat"><span class="cms-stat-value" id="cms-stat-drafts">-</span><span class="cms-stat-label">Drafts</span></div>
|
|
<div class="cms-stat"><span class="cms-stat-value" id="cms-stat-categories">-</span><span class="cms-stat-label">Categories</span></div>
|
|
<div class="cms-stat"><span class="cms-stat-value" id="cms-stat-quizzes">-</span><span class="cms-stat-label">Quizzes</span></div>
|
|
<div class="cms-stat"><span class="cms-stat-value" id="cms-stat-attempts">-</span><span class="cms-stat-label">Attempts</span></div>
|
|
</div>
|
|
|
|
<!-- CMS Layout: sidebar + main -->
|
|
<div class="cms-layout">
|
|
|
|
<!-- Sidebar: Categories + filters -->
|
|
<aside class="cms-sidebar">
|
|
<div class="cms-sidebar-section">
|
|
<h4><i class="fas fa-folder-tree"></i> Categories</h4>
|
|
<div id="lh-cms-categories" class="cms-cat-list"></div>
|
|
<form id="lh-cms-add-category" class="cms-add-cat">
|
|
<input type="text" id="lh-cms-cat-name" placeholder="New category..." aria-label="New category name" class="cms-input-sm">
|
|
<button type="submit" id="btn-lh-add-cat" class="cms-btn-add" title="Add category">Add</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="cms-sidebar-section">
|
|
<h4><i class="fas fa-filter"></i> Filter</h4>
|
|
<select id="cms-filter-status" class="cms-input-sm" style="width:100%;margin-bottom:6px;">
|
|
<option value="all">All Status</option>
|
|
<option value="published">Published</option>
|
|
<option value="draft">Drafts</option>
|
|
</select>
|
|
<select id="cms-filter-category" class="cms-input-sm" style="width:100%;">
|
|
<option value="all">All Categories</option>
|
|
</select>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main: Content list / Editor -->
|
|
<div class="cms-main">
|
|
|
|
<!-- Content list view -->
|
|
<div id="cms-list-view">
|
|
<div class="cms-toolbar">
|
|
<div style="display:flex;gap:8px;">
|
|
<button type="button" id="btn-lh-new-content" class="btn-sm btn-primary" data-type="article"><i class="fas fa-file-alt"></i> New Article</button>
|
|
<button type="button" id="btn-lh-new-quiz" class="btn-sm" style="background:var(--amber-light);color:#92400e;border:1px solid var(--amber);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;"><i class="fas fa-clipboard-question"></i> New Quiz</button>
|
|
<button type="button" id="btn-lh-new-pearl" class="btn-sm" style="background:var(--purple-light);color:var(--purple);border:1px solid var(--purple);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;"><i class="fas fa-gem"></i> New Pearl</button>
|
|
<button type="button" id="btn-lh-new-presentation" class="btn-sm" style="background:#ecfdf5;color:#065f46;border:1px solid #6ee7b7;border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;"><i class="fas fa-presentation-screen"></i> New Presentation</button>
|
|
</div>
|
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
<button type="button" id="btn-lh-refresh-content" class="btn-sm btn-ghost" title="Refresh content list"><i class="fas fa-rotate"></i></button>
|
|
<div class="cms-search">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" id="cms-search" placeholder="Search content..." class="cms-input-sm">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cms-content-table">
|
|
<div class="cms-table-header">
|
|
<span class="cms-col-title">Title</span>
|
|
<span class="cms-col-cat">Category</span>
|
|
<span class="cms-col-type">Type</span>
|
|
<span class="cms-col-status">Status</span>
|
|
<span class="cms-col-date">Updated</span>
|
|
<span class="cms-col-actions"></span>
|
|
</div>
|
|
<div id="lh-cms-content-list" class="cms-table-body">
|
|
<div style="text-align:center;padding:40px;color:var(--g400);">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor view (hidden until edit/new) -->
|
|
<div id="lh-cms-editor" class="cms-editor hidden">
|
|
<div class="cms-editor-header">
|
|
<button type="button" id="btn-lh-close-editor" class="btn-sm btn-ghost"><i class="fas fa-arrow-left"></i> Back to List</button>
|
|
<div class="cms-editor-actions">
|
|
<button type="button" id="btn-lh-ai-open" class="btn-sm" style="background:linear-gradient(135deg,#7c3aed,#2563eb);color:white;border:none;"><i class="fas fa-wand-magic-sparkles"></i> AI Generate</button>
|
|
<button type="button" id="btn-lh-delete-content" class="btn-sm hidden" style="background:var(--red-light);color:var(--red);"><i class="fas fa-trash"></i> Delete</button>
|
|
<select id="lh-cms-edit-published" class="cms-input-sm">
|
|
<option value="false">Draft</option>
|
|
<option value="true">Published</option>
|
|
</select>
|
|
<button type="button" id="btn-lh-save-content" class="btn-sm btn-primary"><i class="fas fa-save"></i> Save</button>
|
|
</div>
|
|
</div>
|
|
<!-- Inline delete confirmation bar — message built dynamically in JS -->
|
|
<div id="lh-delete-confirm-bar" class="lh-delete-confirm-bar hidden">
|
|
<i class="fas fa-triangle-exclamation"></i>
|
|
<span id="lh-delete-confirm-msg"></span>
|
|
<div style="display:flex;gap:8px;margin-left:auto;flex-shrink:0;">
|
|
<button type="button" id="btn-lh-delete-cancel" class="btn-sm btn-ghost">Cancel</button>
|
|
<button type="button" id="btn-lh-delete-yes" class="btn-sm" style="background:var(--red);color:white;">Yes, Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="hidden" id="lh-cms-edit-id" value="">
|
|
|
|
<!-- Title -->
|
|
<input type="text" id="lh-cms-edit-title" class="cms-title-input" placeholder="Enter title...">
|
|
|
|
<!-- Meta row -->
|
|
<div class="cms-meta-row">
|
|
<div class="cms-meta-field">
|
|
<label>Category</label>
|
|
<select id="lh-cms-edit-category" class="cms-input-sm">
|
|
<option value="">Uncategorized</option>
|
|
</select>
|
|
</div>
|
|
<div class="cms-meta-field">
|
|
<label>Subject</label>
|
|
<input type="text" id="lh-cms-edit-subject" class="cms-input-sm" placeholder="e.g., Development">
|
|
</div>
|
|
<div class="cms-meta-field">
|
|
<label>Type</label>
|
|
<select id="lh-cms-edit-type" class="cms-input-sm">
|
|
<option value="article">Article</option>
|
|
<option value="quiz">Quiz Only</option>
|
|
<option value="pearl">Clinical Pearl</option>
|
|
<option value="presentation">Presentation</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Generate panel -->
|
|
<div id="lh-ai-panel" class="lh-ai-panel hidden">
|
|
<div class="lh-ai-panel-header">
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
<span style="font-size:18px;">✨</span>
|
|
<strong style="font-size:14px;color:var(--g800);">AI Content Generator</strong>
|
|
</div>
|
|
<button id="btn-lh-ai-close" class="btn-sm btn-ghost" style="padding:2px 8px;">✕ Close</button>
|
|
</div>
|
|
|
|
<!-- Source tabs -->
|
|
<div class="lh-ai-tabs">
|
|
<button class="lh-ai-tab active" data-aitab="topic"><i class="fas fa-lightbulb"></i> Describe Content</button>
|
|
<button class="lh-ai-tab" data-aitab="upload"><i class="fas fa-file-upload"></i> Upload File</button>
|
|
<button class="lh-ai-tab" data-aitab="webdav" data-feature="nextcloud" id="lh-ai-tab-webdav"><i class="fas fa-cloud"></i> Nextcloud</button>
|
|
</div>
|
|
|
|
<!-- Topic tab -->
|
|
<div class="lh-ai-tabpanel" id="lh-ai-tp-topic">
|
|
<div class="form-group" style="margin:0;">
|
|
<label style="font-size:12px;font-weight:600;color:var(--g600);">Tell AI what to create</label>
|
|
<textarea id="lh-ai-topic" class="cms-input-sm" style="width:100%;min-height:72px;resize:vertical;font-family:inherit;line-height:1.5;" placeholder="Describe the content you want AI to generate, e.g.: Write an article on febrile seizures in children covering causes, evaluation, and management. Create a clinical pearl on neonatal jaundice with key decision points. Build a quiz on pediatric asthma classification and treatment ladder."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload tab -->
|
|
<div class="lh-ai-tabpanel hidden" id="lh-ai-tp-upload">
|
|
<label class="lh-ai-dropzone" id="lh-ai-dropzone">
|
|
<i class="fas fa-cloud-upload-alt" style="font-size:28px;color:var(--blue);margin-bottom:8px;display:block;"></i>
|
|
<span id="lh-ai-file-label">Drop files here or click to browse</span>
|
|
<small style="color:var(--g400);display:block;margin-top:4px;">PDF, DOCX, PPTX, ODT, EPUB, TXT, MD, HTML, CSV, JSON — max 10 MB each, up to 10 files</small>
|
|
<input type="file" id="lh-ai-file" accept=".pdf,.txt,.md,.html,.htm,.csv,.json" multiple style="display:none;">
|
|
</label>
|
|
<div id="lh-ai-files-list" style="margin-top:8px;display:none;"></div>
|
|
<div class="form-group" style="margin:8px 0 0;">
|
|
<label style="font-size:12px;font-weight:600;color:var(--g600);">Topic / context <span style="color:var(--g400);font-weight:400;">(optional — helps AI focus)</span></label>
|
|
<input type="text" id="lh-ai-upload-context" class="cms-input-sm" style="width:100%;" placeholder="e.g., Pediatric asthma management, focus on treatment ladder">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WebDAV tab -->
|
|
<div class="lh-ai-tabpanel hidden" data-feature="nextcloud" id="lh-ai-tp-webdav">
|
|
<!-- File browser — hidden once a file is selected -->
|
|
<div id="lh-ai-webdav-browser">
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
|
<span id="lh-ai-webdav-path-label" style="font-size:12px;color:var(--g500);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">/</span>
|
|
<button id="btn-lh-webdav-refresh" class="btn-sm btn-ghost" style="padding:3px 8px;font-size:12px;"><i class="fas fa-sync"></i></button>
|
|
</div>
|
|
<div id="lh-ai-webdav-list" style="max-height:200px;overflow-y:auto;border:1px solid var(--g200);border-radius:8px;background:white;">
|
|
<div style="padding:12px;text-align:center;color:var(--g400);font-size:13px;">Click to browse your Nextcloud files</div>
|
|
</div>
|
|
</div>
|
|
<!-- Selected file indicator — shown after selection, replaces browser -->
|
|
<div id="lh-ai-webdav-selected" style="display:none;flex-direction:column;gap:8px;">
|
|
<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--blue-light);border-radius:6px;font-size:13px;color:var(--blue);">
|
|
<i class="fas fa-file-check"></i>
|
|
<span id="lh-ai-webdav-selected-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
|
|
<button id="btn-lh-webdav-deselect" style="background:none;border:none;cursor:pointer;color:var(--blue);padding:0;font-size:16px;line-height:1;" title="Remove selection">✕</button>
|
|
</div>
|
|
<div class="form-group" style="margin:0;">
|
|
<label style="font-size:12px;font-weight:600;color:var(--g600);">Topic / context <span style="color:var(--g400);font-weight:400;">(optional — helps AI focus)</span></label>
|
|
<input type="text" id="lh-ai-webdav-context" class="cms-input-sm" style="width:100%;" placeholder="e.g., Focus on management guidelines for pediatric residents">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generation options -->
|
|
<div class="lh-ai-options">
|
|
<!-- Row 1: type + model -->
|
|
<div class="lh-ai-opt-row">
|
|
<div class="lh-ai-opt-field">
|
|
<label>Content type</label>
|
|
<select id="lh-ai-ctype" class="cms-input-sm">
|
|
<option value="article">Article</option>
|
|
<option value="quiz">Quiz</option>
|
|
<option value="pearl">Clinical Pearl</option>
|
|
<option value="presentation">Presentation</option>
|
|
</select>
|
|
</div>
|
|
<div class="lh-ai-opt-field" style="flex:2;">
|
|
<label>Model</label>
|
|
<select id="lh-ai-model" class="cms-input-sm tab-model-select" style="width:100%;"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 2: context-aware size options (shown/hidden via style.display in JS) -->
|
|
<div class="lh-ai-opt-row" id="lh-ai-size-row">
|
|
<div id="lh-ai-words-field" style="display:flex;flex-direction:column;gap:3px;">
|
|
<label style="font-size:11px;font-weight:600;color:var(--g500);text-transform:uppercase;letter-spacing:0.3px;">Approx. word count <span style="color:var(--g400);font-weight:400;">(optional)</span></label>
|
|
<input type="number" id="lh-ai-word-count" value="" min="100" max="3000" step="50" class="cms-input-sm" style="width:110px;" placeholder="e.g. 600">
|
|
</div>
|
|
<div id="lh-ai-slides-field" style="display:none;flex-direction:column;gap:3px;">
|
|
<label style="font-size:11px;font-weight:600;color:var(--g500);text-transform:uppercase;letter-spacing:0.3px;">Number of slides <span style="color:var(--g400);font-weight:400;">(optional)</span></label>
|
|
<input type="number" id="lh-ai-slide-count" value="" min="3" max="30" class="cms-input-sm" style="width:90px;" placeholder="e.g. 10">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 3: quiz questions card -->
|
|
<div id="lh-ai-quiz-row" class="lh-ai-quiz-card">
|
|
<div class="lh-ai-quiz-card-header" id="lh-ai-quiz-card-header">
|
|
<i class="fas fa-clipboard-question" style="color:var(--blue);font-size:14px;"></i>
|
|
<span id="lh-ai-quiz-card-title" style="font-weight:600;font-size:13px;color:var(--g800);">Quiz Questions</span>
|
|
</div>
|
|
<div class="lh-ai-quiz-card-body">
|
|
<label id="lh-ai-q-toggle-label" style="display:none;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--g700);margin:0;">
|
|
<input type="checkbox" id="lh-ai-q-toggle" style="accent-color:var(--blue);width:15px;height:15px;flex-shrink:0;">
|
|
Also generate quiz questions for this content
|
|
</label>
|
|
<div id="lh-ai-q-count-wrap" style="display:flex;align-items:center;gap:8px;">
|
|
<label style="font-size:12px;color:var(--g600);margin:0;">Number of questions</label>
|
|
<input type="number" id="lh-ai-q-count" value="5" min="1" max="20" class="cms-input-sm" style="width:70px;">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 4: grounding -->
|
|
<div class="lh-ai-opt-field" style="width:100%;">
|
|
<label style="display:flex;align-items:center;gap:8px;font-weight:600;">
|
|
<input type="checkbox" id="lh-ai-use-corpus" checked>
|
|
Use the clinical library
|
|
</label>
|
|
<p style="margin:4px 0 0;font-size:12px;color:var(--g500);">
|
|
Searches the indexed clinical corpus for this topic and writes from those
|
|
excerpts, preferring them over the model's own recall. The resource ends with
|
|
a References section listing what it drew on. Turn this off for a topic the
|
|
library does not cover — grounding on the nearest unrelated material is
|
|
worse than not grounding at all.
|
|
</p>
|
|
<p id="lh-ai-grounding-result" role="status" style="margin:6px 0 0;font-size:12px;color:var(--g600);"></p>
|
|
</div>
|
|
|
|
<!-- Row 5: instructions -->
|
|
<div class="lh-ai-opt-field" style="width:100%;">
|
|
<label>Special instructions <span style="color:var(--g400);font-weight:400;">(optional)</span></label>
|
|
<textarea id="lh-ai-refinement" class="cms-input-sm" style="width:100%;min-height:90px;resize:vertical;font-family:inherit;line-height:1.5;" placeholder="e.g., Focus on ER management, suitable for residents, case-based format"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
<button type="button" id="btn-lh-ai-generate" class="btn-sm btn-primary" style="flex:1;padding:10px;">
|
|
<i class="fas fa-wand-magic-sparkles"></i> Generate Content
|
|
</button>
|
|
<button type="button" id="btn-lh-ai-refine-body" class="btn-sm btn-ghost" style="padding:10px;">
|
|
<i class="fas fa-pen-to-square"></i> Refine Body
|
|
</button>
|
|
</div>
|
|
<!-- Inline refine input bar — shown when Refine Body is clicked -->
|
|
<div id="lh-ai-refine-bar" style="display:none;gap:6px;align-items:center;padding:10px 12px;background:#fffbeb;border:1.5px solid var(--amber);border-radius:8px;margin-top:4px;">
|
|
<input type="text" id="lh-ai-refine-input" class="cms-input-sm" style="flex:1;" placeholder='e.g. "Make it more concise" or "Add a clinical case example"'>
|
|
<button id="btn-lh-refine-submit" class="btn-sm btn-primary" style="padding:5px 14px;white-space:nowrap;">Apply</button>
|
|
<button id="btn-lh-refine-cancel" class="btn-sm btn-ghost" style="padding:5px 10px;">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body editor (Tiptap rich text) — hidden for presentations -->
|
|
<div id="lh-body-section" class="cms-body-section">
|
|
<label class="cms-label">Body</label>
|
|
<div id="lh-body-editor" class="cms-quill-body"></div>
|
|
</div>
|
|
|
|
<!-- Marp markdown editor — shown only for presentations -->
|
|
<div id="lh-marp-section" class="cms-body-section hidden">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
|
<label class="cms-label" style="margin:0;">Slides <span style="font-size:11px;color:var(--g400);font-weight:400;">(Marp markdown — separate slides with <code>---</code>)</span></label>
|
|
<div style="display:flex;gap:6px;">
|
|
<button type="button" id="btn-lh-preview-slides" class="btn-sm btn-ghost" style="font-size:12px;"><i class="fas fa-eye"></i> Preview</button>
|
|
<button type="button" id="btn-lh-download-pptx" class="btn-sm" style="background:#065f46;color:white;font-size:12px;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;"><i class="fas fa-file-powerpoint"></i> Download PPTX</button>
|
|
</div>
|
|
</div>
|
|
<textarea id="lh-marp-editor" class="lh-marp-textarea" placeholder="✨ Click 'AI Generate' above to create your slides automatically.
|
|
|
|
After generation, you can edit the Marp markdown here if needed.
|
|
Each slide is separated by ---
|
|
|
|
Example structure:
|
|
# Title Slide
|
|
Subtitle here
|
|
---
|
|
# Topic Slide
|
|
- Key point one
|
|
- Key point two"></textarea>
|
|
</div>
|
|
|
|
<!-- Shared image controls stay visible for both body and presentation editors. -->
|
|
<div id="lh-images-section"></div>
|
|
|
|
<!-- Quiz builder -->
|
|
<div class="cms-quiz-section">
|
|
<div class="cms-quiz-header">
|
|
<h3><i class="fas fa-clipboard-question"></i> Quiz Questions</h3>
|
|
<button type="button" id="btn-lh-add-question" class="btn-sm btn-ghost"><i class="fas fa-plus"></i> Add Question</button>
|
|
</div>
|
|
<div id="lh-cms-questions" class="cms-questions-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|