Skip to content

Commit 2f566a2

Browse files
authored
Merge pull request #477 from owncloud/ext/2026-06-23-ai-data-insights-sidebar
feat(web-app-ai-data-insights-sidebar): add CSV/TSV insights sidebar
2 parents 8132546 + 0e79ef4 commit 2f566a2

30 files changed

Lines changed: 2184 additions & 8 deletions

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ jobs:
101101
- web-app-file-comments
102102
- web-app-ai-folder-brief-sidebar
103103
- web-app-ai-sensitive-data-scanner
104+
- web-app-ai-data-insights-sidebar
104105
steps:
105106
- name: Checkout code
106107
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

dev/docker/ocis.apps.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ ai-sensitive-data-scanner:
3737
llm:
3838
endpoint: 'https://host.docker.internal:9200/ai-llm-proxy/v1'
3939
model: 'llama3.2'
40+
41+
ai-data-insights-sidebar:
42+
config:
43+
llm:
44+
endpoint: 'https://host.docker.internal:9200/ai-llm-proxy/v1'
45+
model: 'llama3.2'

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ services:
5050
- ./packages/web-app-file-comments/dist:/web/apps/file-comments
5151
- ./packages/web-app-ai-folder-brief-sidebar/dist:/web/apps/ai-folder-brief-sidebar
5252
- ./packages/web-app-ai-sensitive-data-scanner/dist:/web/apps/ai-sensitive-data-scanner
53+
- ./packages/web-app-ai-data-insights-sidebar/dist:/web/apps/ai-data-insights-sidebar
5354
depends_on:
5455
- traefik
5556

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# AI CSV/TSV Insights Sidebar
2+
3+
Adds an **Insights** sidebar panel and an **Insights** context-menu action for
4+
CSV and TSV files. On demand it downloads the file via WebDAV, parses it
5+
client-side (headers plus up to the first 200 rows), and sends a compact
6+
structured preview to an admin-configured, OpenAI-compatible LLM endpoint. The
7+
panel renders detected column types, value ranges for numeric columns, and
8+
2–3 natural-language observations about the data.
9+
10+
Files larger than 5 MB are rejected before fetching to avoid buffering large
11+
files in memory. The first analysis per session shows a consent disclosure
12+
informing the user that file contents will be sent to the configured AI service.
13+
14+
No LLM provider is bundled and no API keys are embedded in the browser —
15+
the endpoint is fully operator-controlled (BYO-LLM).
16+
17+
Requests flow **browser → `ai-llm-proxy` sidecar → LLM**. The sidecar
18+
lives in `packages/ai-llm-proxy`, validates the user's oCIS bearer token
19+
against the oCIS OIDC userinfo endpoint, and forwards the request to the
20+
configured LLM with the LLM API key injected server-side. The API key
21+
never reaches the browser.
22+
23+
## Supported File Types
24+
25+
CSV (`.csv`), TSV (`.tsv`)
26+
27+
Files are fetched via WebDAV and parsed client-side with a lightweight
28+
RFC-4180 state machine. The parser handles quoted fields (including embedded
29+
newlines and commas), Windows line endings, and single-column files. At most
30+
30 columns and 5 sample values per column are sent to the LLM, capping prompt
31+
size regardless of file width.
32+
33+
## Extension Points
34+
35+
| ID | Type |
36+
|----|------|
37+
| `global.files.sidebar` | `sidebarPanel` — "Insights" tab, visible for single supported files |
38+
| `global.files.context-actions` | `action` — "Insights" entry that opens the Insights sidebar tab |
39+
40+
Both extension points are hidden entirely when no LLM endpoint is configured,
41+
so users on unconfigured deployments never see empty or broken UI.
42+
43+
## Configuration
44+
45+
### Web App Config
46+
47+
Admins set the proxy endpoint and model in the oCIS Web app config:
48+
49+
```yaml
50+
ai-data-insights-sidebar:
51+
config:
52+
llm:
53+
endpoint: "https://your-ocis.example.com/ai-llm-proxy/v1"
54+
model: "llama3.1:70b"
55+
```
56+
57+
The panel reads `applicationConfig.llm` at startup. If `endpoint` or `model`
58+
is absent both the sidebar panel and the context-menu action are hidden.
59+
60+
### `ai-llm-proxy` Sidecar
61+
62+
The sidecar is configured entirely via environment variables — the LLM API key
63+
stays server-side and never reaches the browser:
64+
65+
| Variable | Required | Description |
66+
|----------|----------|-------------|
67+
| `OCIS_URL` | yes | oCIS base URL, used to discover the OIDC userinfo endpoint |
68+
| `LLM_ENDPOINT` | yes | LLM base URL (OpenAI-compatible, e.g. `http://localhost:11434/v1`) |
69+
| `LLM_API_KEY` | no | API key forwarded to the LLM in `Authorization: Bearer` |
70+
| `PORT` | no | Listening port, default `3030` |
71+
| `NODE_TLS_REJECT_UNAUTHORIZED` | no | Set to `0` for dev stacks with self-signed certs |
72+
73+
In the dev docker-compose stack the sidecar is exposed at
74+
`https://host.docker.internal:9200/ai-llm-proxy/v1` via Traefik.
75+
Set `AI_LLM_ENDPOINT` and `AI_LLM_API_KEY` in a `.env` file or as shell
76+
variables before running `docker-compose up`.
77+
78+
## Insights Output
79+
80+
The LLM is prompted to return a JSON object with three fields:
81+
82+
- **columnTypes** — an array of `{ column, type }` objects confirming the
83+
detected type (`number`, `date`, `boolean`, or `string`) for each column
84+
- **ranges** — an array of `{ column, min, max }` objects for numeric and date
85+
columns
86+
- **observations** — an array of 2–3 plain natural-language observations about
87+
the data
88+
89+
The panel renders this as a column-type table with range annotations and a
90+
bullet list of observations. Observations are returned in the user's preferred
91+
oCIS language (via the `preferredLanguage` profile setting).
92+
93+
## Panel States
94+
95+
| State | Shown when |
96+
|-------|-----------|
97+
| Idle | Panel mounted, no analysis started yet — shows "Analyze" button |
98+
| Analyzing | LLM request in flight — shows "Analyzing…" placeholder |
99+
| Result | Column-type table, ranges, and observations rendered — shows "Re-analyze" button |
100+
| Error | Endpoint unreachable, auth failure, rate-limit, cross-origin block, or malformed response |
101+
102+
Errors are shown only inside the panel and include admin-actionable guidance.
103+
104+
## Security
105+
106+
- The extension enforces a same-origin check on the configured LLM endpoint
107+
before attaching the user's oCIS bearer token. Cross-origin endpoints are
108+
rejected with an in-panel error message.
109+
- No API key is ever stored in or passed through extension config — the key
110+
lives exclusively in the `ai-llm-proxy` server environment.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[main]
2+
host = https://www.transifex.com
3+
4+
[o:owncloud-org:p:owncloud-web:r:web-extensions-ai-data-insights-sidebar]
5+
file_filter = locale/<lang>/app.po
6+
minimum_perc = 0
7+
resource_name = web-extensions-ai-data-insights-sidebar
8+
source_file = template.pot
9+
source_lang = en
10+
type = PO
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
msgid ""
2+
msgstr ""
3+
"Content-Type: text/plain; charset=UTF-8\n"
4+
5+
#: src/composables/useInsights.ts:119
6+
msgid "Access to the AI service was denied. Your session may have expired — try reloading the page."
7+
msgstr ""
8+
9+
#: src/components/InsightsPanel.vue:59
10+
msgid "Analyze this CSV/TSV file to get column insights and observations."
11+
msgstr ""
12+
13+
#: src/components/InsightsPanel.vue:7
14+
msgid "Analyzing…"
15+
msgstr ""
16+
17+
#: src/components/InsightsPanel.vue:39
18+
msgid "Column"
19+
msgstr ""
20+
21+
#: src/composables/useInsights.ts:110
22+
msgid "Could not reach the AI service. Check your network connection and try again."
23+
msgstr ""
24+
25+
#: src/composables/useInsights.ts:161
26+
msgid "Could not resolve file space"
27+
msgstr ""
28+
29+
#: src/components/InsightsPanel.vue:41
30+
msgid "Range"
31+
msgstr ""
32+
33+
#: src/composables/useInsights.ts:147
34+
msgid "Resource location not available"
35+
msgstr ""
36+
37+
#: src/composables/useInsights.ts:137
38+
msgid "Something went wrong while analyzing the file. Please try again."
39+
msgstr ""
40+
41+
#: src/composables/useInsights.ts:124
42+
msgid "The AI endpoint could not be found. Check the endpoint URL in admin settings."
43+
msgstr ""
44+
45+
#: src/composables/useInsights.ts:271
46+
msgid "The AI endpoint must be on the same server as ownCloud. Cross-origin requests are not supported."
47+
msgstr ""
48+
49+
#: src/composables/useInsights.ts:107
50+
msgid "The AI service did not respond in time. Please try again later."
51+
msgstr ""
52+
53+
#: src/composables/useInsights.ts:129
54+
msgid "The AI service is currently busy. Please try again in a moment."
55+
msgstr ""
56+
57+
#: src/composables/useInsights.ts:132
58+
msgid "The AI service is temporarily unavailable. Please try again later."
59+
msgstr ""
60+
61+
#: src/composables/useInsights.ts:175
62+
msgid "The file appears to be empty or has no recognizable columns."
63+
msgstr ""
64+
65+
#: src/composables/useInsights.ts:153
66+
msgid "This file is too large to analyze (limit: 5 MB). Only CSV/TSV files up to 5 MB are supported."
67+
msgstr ""
68+
69+
#: src/components/InsightsPanel.vue:15
70+
msgid "To generate insights, parts of this file will be sent to the AI service configured by your administrator. Continue?"
71+
msgstr ""
72+
73+
#: src/components/InsightsPanel.vue:40
74+
msgid "Type"
75+
msgstr ""
76+
77+
#: src/index.ts:71
78+
msgctxt "AI CSV/TSV Insights Sidebar extension name"
79+
msgid "AI CSV/TSV Insights Sidebar"
80+
msgstr ""
81+
82+
#: src/components/InsightsPanel.vue:62
83+
msgctxt "Button to analyze data file"
84+
msgid "Analyze"
85+
msgstr ""
86+
87+
#: src/components/InsightsPanel.vue:68
88+
msgctxt "Button to re-analyze data file"
89+
msgid "Re-analyze"
90+
msgstr ""
91+
92+
#: src/components/InsightsPanel.vue:25
93+
msgctxt "Consent cancellation button"
94+
msgid "Cancel"
95+
msgstr ""
96+
97+
#: src/components/InsightsPanel.vue:22
98+
msgctxt "Consent confirmation button"
99+
msgid "Send to AI"
100+
msgstr ""
101+
102+
#: src/index.ts:56
103+
msgctxt "Context menu action to open data insights"
104+
msgid "Insights"
105+
msgstr ""
106+
107+
#: src/index.ts:37
108+
msgctxt "Sidebar panel tab title"
109+
msgid "Insights"
110+
msgstr ""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "web-app-ai-data-insights-sidebar",
3+
"version": "0.1.0",
4+
"description": "CANDIDATE",
5+
"license": "Apache-2.0",
6+
"author": "ownCloud",
7+
"type": "module",
8+
"scripts": {
9+
"build": "pnpm vite build",
10+
"build:w": "pnpm vite build --watch --mode development",
11+
"check:types": "vue-tsc --noEmit",
12+
"lint": "eslint src --max-warnings 0",
13+
"test": "NODE_OPTIONS=--unhandled-rejections=throw vitest run",
14+
"test:unit": "NODE_OPTIONS=--unhandled-rejections=throw vitest run",
15+
"test:e2e": "pnpm playwright test"
16+
},
17+
"dependencies": {
18+
"@ownclouders/web-client": "^12.3.2",
19+
"@ownclouders/web-pkg": "^12.3.2"
20+
},
21+
"devDependencies": {
22+
"@ownclouders/extension-sdk": "12.3.2",
23+
"@ownclouders/tsconfig": "0.0.6",
24+
"@types/node": "22.19.19",
25+
"@vue/test-utils": "^2.4.6",
26+
"eslint": "9.39.4",
27+
"happy-dom": "^20.9.0",
28+
"prettier": "3.8.3",
29+
"typescript": "5.9.3",
30+
"vite": "7.2.2",
31+
"vitest": "4.1.7",
32+
"vue": "^3.4.21",
33+
"vue-router": "^5.0.7",
34+
"vue-tsc": "3.3.2",
35+
"vue3-gettext": "^2.4.0"
36+
}
37+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from '@playwright/test'
2+
import baseConfig from '../../playwright.config'
3+
4+
/**
5+
* See https://playwright.dev/docs/test-configuration.
6+
*/
7+
export default defineConfig({
8+
...baseConfig,
9+
testDir: './tests/e2e'
10+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"entrypoint": "index.js"
3+
}

0 commit comments

Comments
 (0)