Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ concurrency:

jobs:
build:
if: github.head_ref != 'release-please--branches--main'
if: |
github.head_ref != 'release-please--branches--main' &&
!contains(github.event.head_commit.message, 'release-please--branches--main')
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ concurrency:

jobs:
build:
if: github.head_ref != 'release-please--branches--main'
if: |
github.head_ref != 'release-please--branches--main' &&
!contains(github.event.head_commit.message, 'release-please--branches--main')
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]

[project]
name = "supernote"
version = "1.4.1"
version = "1.5.0"
license = "Apache-2.0"
license-files = ["LICENSE"]
description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services"
Expand Down
14 changes: 7 additions & 7 deletions supernote/server/static/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="h-full">

<head>
<meta charset="UTF-8">
Expand Down Expand Up @@ -28,8 +28,8 @@
</script>
</head>

<body class="bg-white dark:bg-gray-900 text-black dark:text-white antialiased min-h-screen">
<div id="app" class="flex flex-col min-h-screen" v-cloak>
<body class="h-full bg-white dark:bg-gray-900 text-black dark:text-white antialiased">
<div id="app" class="h-full flex flex-col" v-cloak>
<header class="sticky top-0 z-50 bg-black border-b border-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
Expand Down Expand Up @@ -90,10 +90,10 @@ <h1 class="text-xl font-semibold tracking-tight text-white">Supernote</h1>
</div>
</header>

<main class="flex-grow max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
<main :class="view === 'viewer' ? 'flex-1 min-h-0 flex flex-col overflow-hidden' : 'flex-1 overflow-y-auto max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8'">
<template v-if="isLoggedIn">
<!-- Breadcrumbs + Actions Row -->
<div class="flex items-center justify-between gap-4 mb-8">
<div v-if="view !== 'viewer'" class="flex items-center justify-between gap-4 mb-8">
<nav class="flex items-center overflow-x-auto whitespace-nowrap">
<div v-for="(crumb, index) in breadcrumbs" :key="crumb.id" class="flex items-center">
<span @click="navigateTo(index)"
Expand Down Expand Up @@ -189,8 +189,8 @@ <h3 class="text-lg font-bold text-black dark:text-white mb-4">Create New Folder<
</div>

<!-- Viewer -->
<div v-if="view === 'viewer'" class="min-h-full">
<file-viewer :file="selectedFile" @close="closeViewer"></file-viewer>
<div v-if="view === 'viewer'" class="flex-1 min-h-0 overflow-hidden">
<file-viewer :file="selectedFile" :breadcrumbs="breadcrumbs" @close="closeViewer"></file-viewer>
</div>

<!-- Modals -->
Expand Down
16 changes: 11 additions & 5 deletions supernote/server/static/js/components/FileViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default {
file: {
type: Object,
required: true
},
breadcrumbs: {
type: Array,
default: () => []
}
},
emits: ['close'],
Expand Down Expand Up @@ -180,7 +184,9 @@ export default {
</div>
<div>
<h2 class="text-lg font-bold text-black dark:text-white">{{ file.name }}</h2>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ pages.length }} Pages</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
<span v-if="breadcrumbs.length > 0">{{ breadcrumbs.map(c => c.name).join(' / ') }} &middot; </span>{{ pages.length }} Pages
</p>
</div>
</div>
<div class="flex items-center gap-2">
Expand Down Expand Up @@ -208,7 +214,7 @@ export default {
<!-- Main Content Area -->
<div class="flex-1 overflow-hidden relative flex">
<!-- Pages (Scrollable) -->
<div ref="scrollContainerRef" class="flex-1 overflow-y-auto p-4 sm:p-8">
<div ref="scrollContainerRef" class="flex-1 overflow-y-auto p-4 sm:p-8 snap-y snap-proximity scroll-pt-6">
<div class="max-w-4xl mx-auto">
<!-- Error State -->
<div v-if="error" class="bg-white dark:bg-gray-800 p-12 rounded border border-gray-200 dark:border-gray-700 text-center">
Expand All @@ -227,7 +233,7 @@ export default {

<!-- Pages List -->
<div v-if="!isLoading && !error && pages.length > 0" class="space-y-6">
<div v-for="page in pages" :key="page.pageNo" :data-page-no="page.pageNo" class="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 overflow-hidden">
<div v-for="page in pages" :key="page.pageNo" :data-page-no="page.pageNo" class="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 overflow-hidden snap-start scroll-mt-6">
<div class="border-b border-gray-200 dark:border-gray-700 p-3 bg-gray-50 dark:bg-gray-700 flex justify-between items-center text-xs text-gray-400 font-mono">
<div class="flex items-center gap-2">
<span>Page {{ page.pageNo }}</span>
Expand Down Expand Up @@ -258,8 +264,8 @@ export default {
leave-from-class="translate-x-0"
leave-to-class="translate-x-full"
>
<div v-if="showDetails" class="w-96 border-l border-gray-200 dark:border-gray-700 z-20 absolute right-0 top-0 bottom-0 bg-white dark:bg-gray-800 md:relative">
<summary-panel :file-id="file.id" :active-page="activePage" @close="showDetails = false"></summary-panel>
<div v-show="showDetails" class="w-96 border-l border-gray-200 dark:border-gray-700 z-20 absolute right-0 top-0 bottom-0 bg-white dark:bg-gray-800 md:relative md:flex md:flex-col min-h-0">
<summary-panel :file-id="file.id" :active-page="activePage" @close="showDetails = false" @has-insights="showDetails = true"></summary-panel>
</div>
</transition>
</div>
Expand Down
65 changes: 23 additions & 42 deletions supernote/server/static/js/components/SummaryPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default {
default: 1
}
},
setup(props) {
emits: ['close', 'has-insights'],
setup(props, { emit }) {
const ocrContainerRef = ref(null);
const aiContainerRef = ref(null);

Expand All @@ -36,10 +37,10 @@ export default {

try {
const result = await fetchSummaries(props.fileId);
// Sort by creation time desc
summaries.value = result
.filter(s => (s.dataSource || '').toUpperCase() !== 'OCR')
.sort((a, b) => (b.creationTime || 0) - (a.creationTime || 0));
if (summaries.value.length > 0) emit('has-insights');
} catch (e) {
console.error(e);
error.value = "Failed to load summaries.";
Expand Down Expand Up @@ -70,8 +71,6 @@ export default {
if (tab === 'ocr') loadOcr();
};

// Parse segments from a summary item's metadata field.
// Returns an array of { date_range, summary, page_refs } or null if none.
function parseSegments(item) {
if (!item.metadata) return null;
try {
Expand All @@ -82,8 +81,6 @@ export default {
return null;
}

// For a given summary item, produce the display rows:
// either the parsed segments or a single fallback row.
const aiRows = computed(() => {
const rows = [];
for (const item of summaries.value) {
Expand Down Expand Up @@ -113,10 +110,8 @@ export default {
return rows;
});

// Find the segment index that best matches activePage.
function segmentIndexForPage(pageNo) {
if (!pageNo || aiRows.value.length === 0) return -1;
// Walk forward and return the last segment whose pageRefs contains a page <= pageNo
let best = -1;
for (let i = 0; i < aiRows.value.length; i++) {
const refs = aiRows.value[i].pageRefs;
Expand All @@ -127,18 +122,26 @@ export default {
return best >= 0 ? best : 0;
}

const SCROLL_PADDING = 16; // matches space-y-4 between panel cards

function scrollAiToPage(pageNo) {
if (!aiContainerRef.value) return;
const container = aiContainerRef.value;
if (!container) return;
const idx = segmentIndexForPage(pageNo);
if (idx < 0) return;
const el = aiContainerRef.value.querySelector(`[data-ai-segment="${idx}"]`);
if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
const el = container.querySelector(`[data-ai-segment="${idx}"]`);
if (!el) return;
const top = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop - SCROLL_PADDING;
container.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
}

function scrollOcrToPage(pageNo) {
if (!ocrContainerRef.value || !pageNo) return;
const el = ocrContainerRef.value.querySelector(`[data-ocr-page="${pageNo}"]`);
if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
const container = ocrContainerRef.value;
if (!container || !pageNo) return;
const el = container.querySelector(`[data-ocr-page="${pageNo}"]`);
if (!el) return;
const top = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop - SCROLL_PADDING;
container.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
}

watch([() => props.activePage, activeTab], ([newPage, tab]) => {
Expand All @@ -151,7 +154,6 @@ export default {

onMounted(loadSummaries);
watch(() => props.fileId, () => {
// Reset all state when the viewed file changes
activeTab.value = 'ai';
ocrPages.value = [];
ocrLoaded.value = false;
Expand All @@ -165,7 +167,6 @@ export default {
};

return {
summaries,
isLoading,
error,
activeTab,
Expand All @@ -180,15 +181,14 @@ export default {
};
},
template: `
<div class="h-full flex flex-col bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 w-full md:w-96">
<div class="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 w-full md:w-96">
<!-- Header -->
<div class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700">
<div class="px-4 flex items-center justify-between">
<h3 class="font-semibold text-black dark:text-white flex items-center gap-2 shrink-0">
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
AI Insights
</h3>
<!-- Tabs inline with header -->
<div class="flex gap-1 mx-2">
<button
@click="selectTab('ai')"
Expand Down Expand Up @@ -216,59 +216,40 @@ export default {
</div>

<!-- AI Tab Content -->
<div v-if="activeTab === 'ai'" ref="aiContainerRef" class="flex-1 overflow-y-auto p-4 space-y-4">
<!-- Loading -->
<div v-if="activeTab === 'ai'" ref="aiContainerRef" class="flex-1 min-h-0 overflow-y-auto p-4 space-y-4">
<div v-if="isLoading" class="flex flex-col items-center justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-black dark:border-white mb-3"></div>
<p class="text-sm text-gray-500 dark:text-gray-400">Thinking...</p>
</div>

<!-- Error -->
<div v-if="error" class="bg-gray-100 dark:bg-gray-700 text-black dark:text-white border border-gray-300 dark:border-gray-600 p-4 rounded text-sm">
{{ error }}
</div>

<!-- Empty State -->
<div v-if="!isLoading && !error && aiRows.length === 0" class="text-center py-12">
<p class="text-gray-400 mb-2">No insights yet.</p>
<p class="text-xs text-gray-400">Summaries and transcripts will appear here once processed.</p>
</div>

<!-- Segment rows -->
<div v-for="(row, idx) in aiRows" :key="row.key" :data-ai-segment="idx" class="bg-gray-50 dark:bg-gray-700 rounded p-4 border border-gray-200 dark:border-gray-600">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 min-w-0">
<span class="text-xs font-semibold px-2 py-1 rounded bg-white dark:bg-gray-600 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-500 capitalize shrink-0">
{{ row.dataSource || 'Unknown' }}
</span>
<span v-if="row.dateRange" class="text-xs font-medium text-black dark:text-white truncate">{{ row.dateRange }}</span>
</div>
<span v-if="row.pageRefs.length > 0" class="text-xs text-gray-400 font-mono shrink-0 ml-2">p.{{ row.pageRefs.join(', ') }}</span>
<div v-if="row.dateRange || row.pageRefs.length > 0" class="flex items-center justify-between mb-2">
<span v-if="row.dateRange" class="text-xs font-medium text-black dark:text-white truncate">{{ row.dateRange }}</span>
<span v-if="row.pageRefs.length > 0" class="text-xs text-gray-400 font-mono shrink-0 ml-auto">p.{{ row.pageRefs.join(', ') }}</span>
</div>
<div class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ row.content }}</div>
</div>
</div>

<!-- OCR Tab Content -->
<div v-if="activeTab === 'ocr'" ref="ocrContainerRef" class="flex-1 overflow-y-auto p-4 space-y-4">
<!-- Loading -->
<div v-if="activeTab === 'ocr'" ref="ocrContainerRef" class="flex-1 min-h-0 overflow-y-auto p-4 space-y-4">
<div v-if="isOcrLoading" class="flex flex-col items-center justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-black dark:border-white mb-3"></div>
<p class="text-sm text-gray-500 dark:text-gray-400">Loading OCR text...</p>
</div>

<!-- Error -->
<div v-if="ocrError" class="bg-gray-100 dark:bg-gray-700 text-black dark:text-white border border-gray-300 dark:border-gray-600 p-4 rounded text-sm">
{{ ocrError }}
</div>

<!-- Empty State -->
<div v-if="!isOcrLoading && !ocrError && ocrPages.length === 0" class="text-center py-12">
<p class="text-gray-400 mb-2">No OCR text available.</p>
<p class="text-xs text-gray-400">Text will appear here once the note has been processed.</p>
</div>

<!-- Pages -->
<div v-for="page in ocrPages" :key="page.pageIndex" :data-ocr-page="page.pageIndex + 1" class="bg-gray-50 dark:bg-gray-700 rounded p-4 border border-gray-200 dark:border-gray-600">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-2 font-mono">
Page {{ page.pageIndex + 1 }}
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading