-
Notifications
You must be signed in to change notification settings - Fork 11
feat(ai-project-board-view): add AI Project Space Status Board #520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LukasHirt
wants to merge
12
commits into
main
Choose a base branch
from
ext/2026-07-07-ai-project-board-view
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
24ee9ec
chore(web-app-ai-project-board-view): scaffold package
LukasHirt e0b3052
feat(web-app-ai-project-board-view): Core logic: flesh out `src/compo…
LukasHirt 4d5b42a
fix(web-app-ai-project-board-view): repair failing stage
LukasHirt 1bc9bd7
fix(web-app-ai-project-board-view): repair failing stage
LukasHirt 82ce77d
feat(web-app-ai-project-board-view): UI: build `src/components/BoardC…
LukasHirt 5d39bce
test(web-app-ai-project-board-view): Unit tests: write `tests/unit/us…
LukasHirt ef909aa
test(web-app-ai-project-board-view): E2E tests: replace the skeleton …
LukasHirt 65fe9d5
fix(web-app-ai-project-board-view): repair failing stage
LukasHirt a42abac
fix(web-app-ai-project-board-view): repair failing stage
LukasHirt bbb5440
docs(web-app-ai-project-board-view): Update README.md (and CLAUDE.md …
LukasHirt 3ded12c
chore(web-app-ai-project-board-view): register in docker-compose, CI …
LukasHirt 0b0812b
chore(web-app-ai-project-board-view): align shared dependency version…
LukasHirt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # AI Project Space Status Board | ||
|
|
||
| An ownCloud Infinite Scale (oCIS) folder-view extension that adds a **Status Board** | ||
| view mode to the file list, alongside the built-in "Table" and "Tiles" views. Instead of | ||
| a flat grid, files are grouped into three lanes — **Draft**, **In Review**, and **Final** | ||
| — so a project/space manager can see deliverable status at a glance without opening every | ||
| file. | ||
|
|
||
| The board reuses the file listing the file view has already loaded (no extra WebDAV | ||
| listing call). For each file it sends the name plus a short content excerpt to an | ||
| admin-configured, OpenAI-compatible LLM endpoint, which returns a lane classification per | ||
| file. | ||
|
|
||
| No LLM provider is bundled and no API keys are embedded in the browser — the endpoint is | ||
| fully operator-controlled (BYO-LLM). | ||
|
|
||
| Requests flow **browser → `ai-llm-proxy` sidecar → LLM**. The sidecar validates the user's | ||
| oCIS bearer token and forwards the request to the configured LLM with the API key injected | ||
| server-side. The API key never reaches the browser. | ||
|
|
||
| ## Features | ||
|
|
||
| - Adds a "Status Board" entry to the file view's view-mode switcher | ||
| - Classifies every file into Draft / In Review / Final on load; a **Re-run classification** | ||
| button re-classifies on demand | ||
| - Batches all files in the current folder into a single LLM call per run (not one call per | ||
| file), bounded to the first 60 files — the rest default to Draft and a banner notes the | ||
| truncation | ||
| - Structured output via `response_format: json_object`; falls back to parsing one | ||
| `"<fileId-or-name>: <lane>"` line per file when the LLM returns malformed or non-JSON text | ||
| - Per-file content excerpts (first ~2 KB) are fetched for text-like files only; binary files, | ||
| unsupported mime types, and files above the fetch-size cap fall back to filename-only | ||
| classification | ||
| - Files with no match in either the structured or line-parsed response default to the Draft | ||
| lane rather than being dropped, so every file is always shown somewhere | ||
| - Graceful degradation when no LLM is configured: the board still renders with every file in | ||
| the Draft lane and no network calls are made | ||
| - User's preferred UI language is forwarded to the LLM for any explanatory text (lane values | ||
| themselves are always the English wire vocabulary `draft` / `in-review` / `final`) | ||
|
|
||
| ## Extension Point | ||
|
|
||
| | ID | Type | | ||
| |----|------| | ||
| | `app.files.folder-views.folder` | `folderView` — "Status Board" view mode | | ||
|
|
||
| `app.files.folder-views.project-spaces` (the project-spaces *overview*, i.e. the grid of | ||
| space tiles) has no files to classify. The file listing inside an opened space — where this | ||
| board actually renders — is the generic `app.files.folder-views.folder` extension point, so | ||
| the board is registered there. | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Web App Config | ||
|
|
||
| Admins set the proxy endpoint and model in the oCIS Web app config. The key **must** match | ||
| the Docker mount target directory (`ai-project-board-view`), not the package name: | ||
|
|
||
| ```yaml | ||
| ai-project-board-view: | ||
| config: | ||
| llm: | ||
| endpoint: "https://your-ocis.example.com/ai-llm-proxy/v1" | ||
| model: "llama3.1:70b" | ||
| ``` | ||
|
|
||
| The board reads `applicationConfig.llm` at startup. If `endpoint` or `model` is missing, the | ||
| Status Board view mode is still available but every file is shown in the Draft lane and no | ||
| LLM calls are made. | ||
|
|
||
| ### `ai-llm-proxy` Sidecar | ||
|
|
||
| The sidecar is configured entirely via environment variables — the LLM API key stays | ||
| server-side and never reaches the browser: | ||
|
|
||
| | Variable | Required | Description | | ||
| |----------|----------|--------------| | ||
| | `OCIS_URL` | yes | oCIS base URL, used to validate OIDC bearer tokens | | ||
| | `LLM_ENDPOINT` | yes | LLM base URL (OpenAI-compatible, e.g. `http://localhost:11434/v1`) | | ||
| | `LLM_API_KEY` | no | API key forwarded to the LLM in `Authorization: Bearer` | | ||
| | `PORT` | no | Listening port, default `3030` | | ||
| | `NODE_TLS_REJECT_UNAUTHORIZED` | no | Set to `0` for dev stacks with self-signed certs | | ||
|
|
||
| In the dev docker-compose stack the sidecar is exposed at | ||
| `https://host.docker.internal:9200/ai-llm-proxy/v1` via Traefik. | ||
|
|
||
| ## Classification Output | ||
|
|
||
| The LLM is prompted to return a JSON object with one key: | ||
|
|
||
| - **classifications** — an array of `{ fileId, lane }` objects, where `fileId` matches a | ||
| file's resource ID verbatim and `lane` is one of `draft`, `in-review`, `final` | ||
|
|
||
| When the response is not valid JSON, the raw text is parsed line by line | ||
| (`"<fileId-or-name>: <lane>"`, matched against keywords `final`/`approved`/`done`, | ||
| `review`/`in-review`, `draft`) instead. | ||
|
|
||
| ## Board States | ||
|
|
||
| | State | Shown when | | ||
| |-------|-----------| | ||
| | Loading | Classification request in flight — spinner over the lane area | | ||
| | Board | Classification complete — three lanes with file cards, "Re-run classification" enabled | | ||
| | Empty | The current folder has no files | | ||
| | Unconfigured notice | No LLM endpoint configured — all files shown in Draft | | ||
| | Error | Endpoint unreachable, auth failure, rate-limit, cross-origin block, or malformed response | | ||
| | Truncated notice | More than 60 files in the folder — only the first 60 were classified | | ||
|
|
||
| Errors are shown inside the board and include admin-actionable guidance (e.g. "Check the | ||
| endpoint URL in admin settings"). | ||
|
|
||
| ## Security | ||
|
|
||
| - The extension enforces a same-origin check on the configured LLM endpoint before attaching | ||
| the user's oCIS bearer token. Cross-origin endpoints are rejected with an in-board error | ||
| message instead of being called. | ||
| - No API key is ever stored in or passed through extension config — the key lives | ||
| exclusively in the `ai-llm-proxy` server environment. | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| # Install dependencies (from repo root) | ||
| pnpm install | ||
|
|
||
| # Build the extension | ||
| pnpm -F web-app-ai-project-board-view build | ||
|
|
||
| # Type check | ||
| pnpm -F web-app-ai-project-board-view check:types | ||
|
|
||
| # Unit tests | ||
| pnpm -F web-app-ai-project-board-view test:unit | ||
|
|
||
| # E2E tests (requires a running oCIS instance — see root README for setup) | ||
| pnpm -F web-app-ai-project-board-view test:e2e | ||
| ``` | ||
|
|
||
| See the root `README.md` for how to run a local oCIS development environment with the | ||
| extension loaded. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [main] | ||
| host = https://www.transifex.com | ||
|
|
||
| [o:owncloud-org:p:owncloud-web:r:web-extensions-ai-project-board-view] | ||
| file_filter = locale/<lang>/app.po | ||
| minimum_perc = 0 | ||
| resource_name = web-extensions-ai-project-board-view | ||
| source_file = template.pot | ||
| source_lang = en | ||
| type = PO |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "name": "web-app-ai-project-board-view", | ||
| "version": "0.1.0", | ||
| "description": "AI Project Space Status Board", | ||
| "license": "Apache-2.0", | ||
| "author": "ownCloud", | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "pnpm vite build", | ||
| "build:w": "pnpm vite build --watch --mode development", | ||
| "check:types": "vue-tsc --noEmit", | ||
| "test:unit": "NODE_OPTIONS=--unhandled-rejections=throw vitest run", | ||
| "test:e2e": "pnpm playwright test" | ||
| }, | ||
| "dependencies": { | ||
| "@ownclouders/web-client": "^12.5.0", | ||
| "@ownclouders/web-pkg": "^12.5.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@ownclouders/extension-sdk": "12.5.0", | ||
| "@ownclouders/tsconfig": "0.0.6", | ||
| "@types/node": "22.19.19", | ||
| "@vue/test-utils": "^2.4.6", | ||
| "eslint": "9.39.4", | ||
| "happy-dom": "^20.11.0", | ||
| "prettier": "3.9.5", | ||
| "typescript": "5.9.3", | ||
| "vite": "7.2.2", | ||
| "vitest": "4.1.10", | ||
| "vue": "^3.5.40", | ||
| "vue-router": "^5.2.0", | ||
| "vue-tsc": "3.3.7", | ||
| "vue3-gettext": "^2.4.0" | ||
| } | ||
| } |
22 changes: 22 additions & 0 deletions
22
packages/web-app-ai-project-board-view/playwright.config.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { defineConfig } from '@playwright/test' | ||
| import baseConfig from '../../playwright.config' | ||
|
|
||
| /** | ||
| * See https://playwright.dev/docs/test-configuration. | ||
| */ | ||
| export default defineConfig({ | ||
| ...baseConfig, | ||
| testDir: './tests/e2e', | ||
| use: { | ||
| ...baseConfig.use, | ||
| // Trace mode is forced via `--trace` on the gate's own invocation | ||
| // (gate/run-gate.sh) instead of here, so it doesn't ship in every | ||
| // extension's committed config. | ||
| // | ||
| // Full video/screenshot capture only during the gate's CI run (CI=true | ||
| // is set by gate/run-gate.sh) for demo-media generation; local | ||
| // `pnpm test:e2e` stays lightweight. | ||
| screenshot: process.env.CI ? 'on' : 'only-on-failure', | ||
| video: process.env.CI ? 'on' : 'off' | ||
| } | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "entrypoint": "index.js" | ||
| } |
61 changes: 61 additions & 0 deletions
61
packages/web-app-ai-project-board-view/src/components/BoardCard.vue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| <template> | ||
| <div class="board-card oc-rounded oc-p-s oc-mb-s" data-testid="board-card"> | ||
| <div class="oc-flex oc-flex-middle oc-gap-xs"> | ||
| <resource-icon :resource="resource" size="small" /> | ||
| <span class="board-card-name oc-text-truncate" :title="resource.name"> | ||
| {{ resource.name }} | ||
| </span> | ||
| </div> | ||
| <p class="board-card-mdate oc-text-muted oc-my-rm oc-mt-xs"> | ||
| {{ lastModifiedLabel }} | ||
| </p> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { computed } from 'vue' | ||
| import { useGettext } from 'vue3-gettext' | ||
| import { ResourceIcon } from '@ownclouders/web-pkg' | ||
| import type { Resource } from '@ownclouders/web-client' | ||
|
|
||
| const props = defineProps<{ resource: Resource }>() | ||
|
|
||
| const { $gettext, current: currentLanguage } = useGettext() | ||
|
|
||
| function formatMDate(mdate?: string): string | undefined { | ||
| if (!mdate) { | ||
| return undefined | ||
| } | ||
| const date = new Date(mdate) | ||
| if (isNaN(date.getTime())) { | ||
| return undefined | ||
| } | ||
| return date.toLocaleDateString(currentLanguage, { | ||
| year: 'numeric', | ||
| month: 'short', | ||
| day: 'numeric' | ||
| }) | ||
| } | ||
|
|
||
| const lastModifiedLabel = computed(() => { | ||
| const formatted = formatMDate(props.resource.mdate) | ||
| return formatted | ||
| ? $gettext('Modified %{date}', { date: formatted }) | ||
| : $gettext('Modification date unknown') | ||
| }) | ||
| </script> | ||
|
|
||
| <style scoped> | ||
| .board-card { | ||
| border: 1px solid var(--oc-color-border, #e0e0e0); | ||
| background-color: var(--oc-color-background-default, #fff); | ||
| } | ||
| .board-card-name { | ||
| flex: 1 1 auto; | ||
| min-width: 0; | ||
| font-weight: 600; | ||
| } | ||
| .board-card-mdate { | ||
| font-size: 0.8rem; | ||
| } | ||
| </style> |
66 changes: 66 additions & 0 deletions
66
packages/web-app-ai-project-board-view/src/components/BoardLane.vue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| <template> | ||
| <section | ||
| class="board-lane oc-rounded oc-p-s oc-background-muted" | ||
| :data-testid="`board-lane-${lane}`" | ||
| :aria-label="label" | ||
| > | ||
| <header class="board-lane-header oc-flex oc-flex-middle oc-flex-between oc-mb-s"> | ||
| <div class="oc-flex oc-flex-middle oc-gap-xs"> | ||
| <oc-icon :name="icon.name" :fill-type="icon.fillType" /> | ||
| <span class="board-lane-label">{{ label }}</span> | ||
| </div> | ||
| <span class="board-lane-count oc-rounded oc-background-default" data-testid="board-lane-count"> | ||
| {{ resources.length }} | ||
| </span> | ||
| </header> | ||
|
|
||
| <div class="board-lane-cards"> | ||
| <board-card v-for="resource in resources" :key="resource.id" :resource="resource" /> | ||
| <p v-if="!resources.length" class="board-lane-empty oc-text-muted"> | ||
| {{ $gettext('No files in this lane yet.') }} | ||
| </p> | ||
| </div> | ||
| </section> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { useGettext } from 'vue3-gettext' | ||
| import type { Resource } from '@ownclouders/web-client' | ||
| import type { IconType } from '@ownclouders/web-pkg' | ||
| import BoardCard from './BoardCard.vue' | ||
|
|
||
| defineProps<{ | ||
| lane: string | ||
| label: string | ||
| icon: IconType | ||
| resources: Resource[] | ||
| }>() | ||
|
|
||
| const { $gettext } = useGettext() | ||
| </script> | ||
|
|
||
| <style scoped> | ||
| .board-lane { | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1 1 0; | ||
| min-width: 16rem; | ||
| min-height: 0; | ||
| } | ||
| .board-lane-label { | ||
| font-weight: 600; | ||
| } | ||
| .board-lane-count { | ||
| font-size: 0.75rem; | ||
| padding: 0.1rem 0.5rem; | ||
| } | ||
| .board-lane-cards { | ||
| overflow-y: auto; | ||
| flex: 1 1 auto; | ||
| min-height: 0; | ||
| } | ||
| .board-lane-empty { | ||
| font-style: italic; | ||
| font-size: 0.85rem; | ||
| } | ||
| </style> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make it and env variable please