dmartincy commited on
Commit
e27f03b
·
1 Parent(s): c6d4e5d

Add document authoring

Browse files
Dockerfile CHANGED
@@ -1,5 +1,12 @@
1
  FROM pspdfkit/ai-assistant:nightly
2
 
 
 
 
 
 
 
 
3
  # Set environment variables
4
  ENV HOME=/home/node \
5
  PNPM_HOME=/root/.local/share/pnpm \
@@ -66,19 +73,15 @@ RUN mkdir -p /var/cache/nginx \
66
  /run/nginx
67
 
68
  # Copy application files to /base BEFORE switching user
69
- COPY --chown=node:node app.py service-config.yml /base/app/
70
  COPY --chown=node:node nginx.conf /etc/nginx/nginx.conf
71
- COPY --chown=node:node healthcheck.sh start-services.sh /base/app/
72
  COPY --chown=node:node service-config.yml /service-config.yml
73
 
74
- # Create app directories and set up Python
75
  USER node
76
  RUN mkdir -p $HOME/models && \
77
- mkdir -p $HOME/pgdata && \
78
- python3 -m venv $HOME/venv && \
79
- $HOME/venv/bin/pip install flask
80
 
81
- # pgvector installation (needs root)
82
  USER root
83
  RUN git clone https://github.com/pgvector/pgvector.git && \
84
  cd pgvector && \
@@ -91,9 +94,45 @@ RUN git clone https://github.com/pgvector/pgvector.git && \
91
  USER node
92
  RUN wget -q https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf -O $HOME/models/gemma-2b.gguf && \
93
  wget -q https://huggingface.co/leliuga/all-MiniLM-L6-v2-GGUF/resolve/main/all-MiniLM-L6-v2.F16.gguf -O $HOME/models/embeddings.gguf && \
94
- chmod +x /base/app/healthcheck.sh /base/app/start-services.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- EXPOSE 5432 7860 4000 8080 8081 8082
 
97
 
98
  USER root
99
 
 
1
  FROM pspdfkit/ai-assistant:nightly
2
 
3
+ # The main ai assistant JS app is in /base/app.
4
+ # Then I have a document authoring web app that is a Vite server that serves index.html and
5
+ # communicates with the ai assistant.
6
+ #
7
+ # Where should we put this web app? In /base/app it's gonna be problematic.
8
+ # New folder? /docauth?
9
+
10
  # Set environment variables
11
  ENV HOME=/home/node \
12
  PNPM_HOME=/root/.local/share/pnpm \
 
73
  /run/nginx
74
 
75
  # Copy application files to /base BEFORE switching user
 
76
  COPY --chown=node:node nginx.conf /etc/nginx/nginx.conf
77
+ COPY --chown=node:node start-services.sh /base/app/
78
  COPY --chown=node:node service-config.yml /service-config.yml
79
 
 
80
  USER node
81
  RUN mkdir -p $HOME/models && \
82
+ mkdir -p $HOME/pgdata
 
 
83
 
84
+ # Install pgvector (needs root)
85
  USER root
86
  RUN git clone https://github.com/pgvector/pgvector.git && \
87
  cd pgvector && \
 
94
  USER node
95
  RUN wget -q https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf -O $HOME/models/gemma-2b.gguf && \
96
  wget -q https://huggingface.co/leliuga/all-MiniLM-L6-v2-GGUF/resolve/main/all-MiniLM-L6-v2.F16.gguf -O $HOME/models/embeddings.gguf && \
97
+ chmod +x /base/app/start-services.sh
98
+
99
+ USER root
100
+ # Fix permissions for scripts and pnpm directory
101
+ RUN chmod +x /base/app/start-services.sh && \
102
+ mkdir -p /root/.local/share/pnpm && \
103
+ chmod 755 /root && \
104
+ chmod 755 /root/.local && \
105
+ chmod 755 /root/.local/share && \
106
+ chmod -R 755 /root/.local/share/pnpm && \
107
+ chown -R node:node /root/.local/share/pnpm
108
+
109
+ # Set up document authoring in a separate directory
110
+ WORKDIR /docauth
111
+
112
+ # Create directory and set permissions
113
+ USER root
114
+ RUN mkdir -p /docauth && \
115
+ chown -R node:node /docauth && \
116
+ chmod -R 755 /docauth
117
+
118
+ USER node
119
+
120
+ # Copy package files
121
+ COPY --chown=node:node package*.json ./
122
+ COPY --chown=node:node tsconfig*.json ./
123
+ COPY --chown=node:node vite.config.ts ./
124
+ COPY --chown=node:node .eslintrc.cjs ./
125
+
126
+ # Install dependencies
127
+ RUN npm install
128
+
129
+ # Copy source files
130
+ COPY --chown=node:node src ./src
131
+ COPY --chown=node:node public ./public
132
+ COPY --chown=node:node index.html ./
133
 
134
+ # Expose Vite's port (using 7860 as configured in vite.config.ts)
135
+ EXPOSE 7860
136
 
137
  USER root
138
 
Modelfile DELETED
@@ -1,2 +0,0 @@
1
- FROM /models/gemma-2-2b-it-Q4_K_M.gguf
2
-
 
 
 
Modelfile.embeddings DELETED
@@ -1 +0,0 @@
1
- FROM /models/mxbai-embed-large-v1-f16.gguf
 
 
app.py DELETED
@@ -1,10 +0,0 @@
1
- from flask import Flask
2
-
3
- app = Flask(__name__)
4
-
5
- @app.route('/')
6
- def home():
7
- return "PostgreSQL is running with Flask!"
8
-
9
- if __name__ == '__main__':
10
- app.run(host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
 
 
index.html CHANGED
@@ -5,10 +5,107 @@
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Document Authoring SDK</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
- <ul>
11
- <li><a href='./javascript.html'>JavaScript example</a></li>
12
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  </body>
14
  </html>
 
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Document Authoring SDK</title>
8
+ <style>
9
+ .spinner {
10
+ display: inline-block;
11
+ width: 16px;
12
+ height: 16px;
13
+ border: 2px solid #f3f3f3;
14
+ border-top: 2px solid #3498db;
15
+ border-radius: 50%;
16
+ animation: spin 1s linear infinite;
17
+ margin-right: 8px;
18
+ vertical-align: middle;
19
+ }
20
+
21
+ @keyframes spin {
22
+ 0% { transform: rotate(0deg); }
23
+ 100% { transform: rotate(360deg); }
24
+ }
25
+
26
+ #translateButton:disabled {
27
+ cursor: not-allowed;
28
+ opacity: 0.7;
29
+ }
30
+ </style>
31
  </head>
32
  <body>
33
+ <button id="translateButton">Translate to English</button>
34
+ <div id="editor" style="border: 1px solid black; width: 1024px; height: 600px; position: relative"></div>
35
+
36
+ <script src="https://document-authoring-cdn.pspdfkit.com/releases/document-authoring-1.0.26-umd.js" crossorigin></script>
37
+ <script>
38
+ (async () => {
39
+ const docAuthSystem = await DocAuth.createDocAuthSystem({
40
+ // assets: { base: '<CUSTOM_DOCUMENT_AUTHORING_ASSETS_LOCATION>' },
41
+ // licenseKey: '<YOUR_LICENSE_KEY>',
42
+ });
43
+
44
+ const editor = await docAuthSystem.createEditor(
45
+ document.getElementById("editor"),
46
+ {
47
+ document: await docAuthSystem.createDocumentFromPlaintext('Hi there!\n\nHow are you?'),
48
+ }
49
+ );
50
+
51
+ // Translation function.
52
+ async function translate(content, lang = "English") {
53
+ try {
54
+ const response = await fetch('/api/aia/v1/chat/completions', {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'Authorization': 'Token token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzMyNjQ2MDcsImV4cCI6MTczMzUyMzgwN30.WWyks6NOU43_2OcZX82z3P4QUwQiUA61SeTZsNobIGoVg3uDTkedXWqISIB7QqTnRyf_D92t_pGr5g_GzqBaOTqaXYJJPco9mtZjtAyUpdWr1UHu4jx-zoKUpCeguZQZbHG6vqYzu1BNV1iPdij2OIoUrr_DY--G-6m5-31E0LcDZb7ysUyZL1Ai_q7PPRwTS8mEtvuF4QO6Cw_a77X7Qh9x_V8a_WwNMFwQMRkIUAsRx3IOI10q0hdwP-k8xYtTbHQO-z66pRl6I71ATclR7FS5FraoLLmWlq8lqNKfzeOgER8Kcf8uB87zHQbfk0QR8AfJGmZDE8pG5F3QG29MeA'
59
+ },
60
+ body: JSON.stringify({
61
+ messages: [
62
+ {
63
+ role: "system",
64
+ content: "You are a translator. Only provide the translation, no explanations or additional text."
65
+ },
66
+ {
67
+ role: "user",
68
+ content: `Translate the following text to ${lang}: ${content}`
69
+ }
70
+ ],
71
+ model: "gemma-2b",
72
+ stream: false
73
+ })
74
+ });
75
+
76
+ if (!response.ok) {
77
+ throw new Error('Translation request failed');
78
+ }
79
+
80
+ const data = await response.json();
81
+ console.log('Translation API response:', data);
82
+ return data.choices[0].message.content;
83
+ } catch (error) {
84
+ console.error("Translation error:", error);
85
+ throw new Error("Failed to generate translation");
86
+ }
87
+ }
88
+
89
+ async function testDummy() {
90
+ try {
91
+ const response = await fetch('/dummy');
92
+ const text = await response.text();
93
+ console.log('Dummy response:', text);
94
+ } catch (error) {
95
+ console.error('Dummy error:', error);
96
+ }
97
+ }
98
+
99
+ // Dev exports
100
+ window.DocAuth = DocAuth;
101
+ window.docAuthSystem = docAuthSystem;
102
+ window.editor = editor;
103
+
104
+ const testButton = document.createElement('button');
105
+ testButton.textContent = 'Test Dummy';
106
+ document.body.insertBefore(testButton, document.getElementById('editor'));
107
+ testButton.addEventListener('click', testDummy);
108
+ })();
109
+ </script>
110
  </body>
111
  </html>
nginx.conf CHANGED
@@ -4,8 +4,18 @@ events {
4
 
5
  http {
6
  server {
7
- listen 8080;
8
 
 
 
 
 
 
 
 
 
 
 
9
  location /v1/embeddings {
10
  proxy_pass http://127.0.0.1:8081;
11
  }
 
4
 
5
  http {
6
  server {
7
+ listen 7860;
8
 
9
+ location / {
10
+ return 200 'Hello World!<br><a href="https://dmartincy-document-translation.hf.space/dummy">Go to dummy page</a>';
11
+ add_header Content-Type text/html;
12
+ }
13
+
14
+ location /dummy {
15
+ return 200 'Hello dummy';
16
+ add_header Content-Type text/html;
17
+ }
18
+
19
  location /v1/embeddings {
20
  proxy_pass http://127.0.0.1:8081;
21
  }
public/javascript-app.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import DocAuth from "https://document-authoring-cdn.pspdfkit.com/releases/document-authoring-1.0.26-esm.js";
2
+
3
+ const docAuthSystem = await DocAuth.createDocAuthSystem({
4
+ // assets: { base: '<CUSTOM_DOCUMENT_AUTHORING_ASSETS_LOCATION>' },
5
+ // licenseKey: '<YOUR_LICENSE_KEY>',
6
+ });
7
+
8
+ const editor = await docAuthSystem.createEditor(
9
+ document.getElementById("editor"),
10
+ {
11
+ document: await docAuthSystem.importDOCX(fetch("./Spanish.docx")),
12
+ }
13
+ );
14
+
15
+ // Translation function.
16
+ async function translate(content, lang = "English") {
17
+ try {
18
+ const response = await fetch('/api/aia/v1/chat/completions', {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ 'Authorization': 'Token token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzMyNjQ2MDcsImV4cCI6MTczMzUyMzgwN30.WWyks6NOU43_2OcZX82z3P4QUwQiUA61SeTZsNobIGoVg3uDTkedXWqISIB7QqTnRyf_D92t_pGr5g_GzqBaOTqaXYJJPco9mtZjtAyUpdWr1UHu4jx-zoKUpCeguZQZbHG6vqYzu1BNV1iPdij2OIoUrr_DY--G-6m5-31E0LcDZb7ysUyZL1Ai_q7PPRwTS8mEtvuF4QO6Cw_a77X7Qh9x_V8a_WwNMFwQMRkIUAsRx3IOI10q0hdwP-k8xYtTbHQO-z66pRl6I71ATclR7FS5FraoLLmWlq8lqNKfzeOgER8Kcf8uB87zHQbfk0QR8AfJGmZDE8pG5F3QG29MeA'
23
+ },
24
+ body: JSON.stringify({
25
+ messages: [
26
+ {
27
+ role: "system",
28
+ content: "You are a translator. Only provide the translation, no explanations or additional text."
29
+ },
30
+ {
31
+ role: "user",
32
+ content: `Translate the following text to ${lang}: ${content}`
33
+ }
34
+ ],
35
+ model: "gemma-2b",
36
+ stream: false
37
+ })
38
+ });
39
+
40
+ if (!response.ok) {
41
+ throw new Error('Translation request failed');
42
+ }
43
+
44
+ const data = await response.json();
45
+ console.log('Translation API response:', data);
46
+ return data.choices[0].message.content;
47
+ } catch (error) {
48
+ console.error("Translation error:", error);
49
+ throw new Error("Failed to generate translation");
50
+ }
51
+ }
52
+
53
+ // Function to recursively translate text in the JSON structure.
54
+ async function translateRecursive(obj) {
55
+ for (let key in obj) {
56
+ if (typeof obj[key] === "string" && key === "text" && obj[key].length > 0) {
57
+ obj[key] = await translate(obj[key]);
58
+ } else if (typeof obj[key] === "object") {
59
+ await translateRecursive(obj[key]);
60
+ }
61
+ }
62
+ }
63
+
64
+ // Function to translate the document.
65
+ async function translateDocument() {
66
+ try {
67
+ translateButton.disabled = true;
68
+ translateButton.innerHTML = '<span class="spinner"></span> Translating...';
69
+
70
+ const jsonDoc = await editor.currentDocument().saveDocument();
71
+ await translateRecursive(jsonDoc.container.document);
72
+
73
+ const translatedDoc = await docAuthSystem.loadDocument(jsonDoc);
74
+ editor.setCurrentDocument(translatedDoc);
75
+ } catch (error) {
76
+ console.error("Translation error:", error);
77
+ } finally {
78
+ translateButton.disabled = false;
79
+ translateButton.innerHTML = 'Translate to English';
80
+ }
81
+ }
82
+
83
+ // Add click event listener to the translate button.
84
+ const translateButton = document.getElementById("translateButton");
85
+ translateButton.addEventListener("click", translateDocument);
86
+
87
+ // Dev exports.
88
+ window.DocAuth = DocAuth;
89
+ window.docAuthSystem = docAuthSystem;
90
+ window.editor = editor;
service-config.yml CHANGED
@@ -4,17 +4,17 @@ aiServices:
4
  chat:
5
  provider:
6
  name: 'openai-compat'
7
- baseUrl: http://127.0.0.1:8080/v1
8
  model: 'gemma-2b'
9
  textEmbeddings:
10
  provider:
11
  name: 'openai-compat'
12
- baseUrl: http://127.0.0.1:8080/v1
13
  model: 'all-MiniLM-L6-v2'
14
  headless:
15
  - provider:
16
  name: 'openai-compat'
17
- baseUrl: http://127.0.0.1:8080/v1
18
  model:
19
  name: 'gemma-2b'
20
  id: 'gemma-2b'
 
4
  chat:
5
  provider:
6
  name: 'openai-compat'
7
+ baseUrl: http://127.0.0.1:7861/v1
8
  model: 'gemma-2b'
9
  textEmbeddings:
10
  provider:
11
  name: 'openai-compat'
12
+ baseUrl: http://127.0.0.1:7861/v1
13
  model: 'all-MiniLM-L6-v2'
14
  headless:
15
  - provider:
16
  name: 'openai-compat'
17
+ baseUrl: http://127.0.0.1:7861/v1
18
  model:
19
  name: 'gemma-2b'
20
  id: 'gemma-2b'
start-container.sh DELETED
@@ -1,28 +0,0 @@
1
- #!/bin/sh
2
-
3
- # Start nginx
4
- nginx
5
-
6
- # Start llamafiler instances
7
- /usr/local/bin/llamafiler --model /base/gemma-2b.gguf --listen 0.0.0.0:8082 &
8
- GEMMA_PID=$!
9
-
10
- /usr/local/bin/llamafiler --model /base/embeddings.gguf --listen 0.0.0.0:8081 &
11
- EMBEDDINGS_PID=$!
12
-
13
- # Wait for services
14
- until curl -s --fail -X POST http://localhost:8080/v1/chat/completions \
15
- -H "Content-Type: application/json" \
16
- -d '{"model": "gemma-2b", "messages":[{"role":"user","content":"hi"}]}' >/dev/null 2>&1 && \
17
- curl -s --fail -X POST http://localhost:8080/v1/embeddings \
18
- -H "Content-Type: application/json" \
19
- -d '{"input":"test"}' >/dev/null 2>&1
20
- do
21
- sleep 1
22
- echo "Waiting for services..."
23
- done
24
-
25
- echo 'Services are ready!'
26
-
27
- # Start the main application
28
- exec ./bin/entrypoint.sh node app/main.bundle.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start-services.sh CHANGED
@@ -46,6 +46,27 @@ done
46
 
47
  echo "PostgreSQL is ready!"
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  # 4. Start Llamafiler (reusing existing code)
50
  echo "Starting llamafiler services..."
51
  mkdir -p /tmp/llamafiler
@@ -63,10 +84,10 @@ echo "Waiting for initial model loading..."
63
  sleep 15 # Give more time for the models to load into memory
64
 
65
  echo 'Waiting for servers to start...'
66
- until curl -s --fail -X POST http://localhost:8080/v1/chat/completions \
67
  -H "Content-Type: application/json" \
68
  -d '{"model": "gemma-2b", "messages":[{"role":"user","content":"hi"}]}' >/dev/null 2>&1 && \
69
- curl -s --fail -X POST http://localhost:8080/v1/embeddings \
70
  -H "Content-Type: application/json" \
71
  -d '{"input":"test"}' >/dev/null 2>&1
72
  do
 
46
 
47
  echo "PostgreSQL is ready!"
48
 
49
+ # Start the web app
50
+ echo "Starting web app..."
51
+ cd /docauth
52
+ export NODE_OPTIONS="--experimental-vm-modules --experimental-json-modules"
53
+
54
+ # Start vite with the existing config.
55
+ npx vite serve --host 0.0.0.0 --port 7860 &
56
+
57
+ # Wait for the web app to start
58
+ echo "Waiting for web app..."
59
+ until curl -s http://127.0.0.1:7860 > /dev/null; do
60
+ echo "Waiting for web app..."
61
+ sleep 2
62
+ if [ $SECONDS -gt 30 ]; then
63
+ echo "Timeout waiting for web app"
64
+ exit 1
65
+ fi
66
+ done
67
+
68
+ echo "Web app is ready!"
69
+
70
  # 4. Start Llamafiler (reusing existing code)
71
  echo "Starting llamafiler services..."
72
  mkdir -p /tmp/llamafiler
 
84
  sleep 15 # Give more time for the models to load into memory
85
 
86
  echo 'Waiting for servers to start...'
87
+ until curl -s --fail -X POST http://127.0.0.1:7861/v1/chat/completions \
88
  -H "Content-Type: application/json" \
89
  -d '{"model": "gemma-2b", "messages":[{"role":"user","content":"hi"}]}' >/dev/null 2>&1 && \
90
+ curl -s --fail -X POST http://127.0.0.1:7861/v1/embeddings \
91
  -H "Content-Type: application/json" \
92
  -d '{"input":"test"}' >/dev/null 2>&1
93
  do
vite.config.ts CHANGED
@@ -8,14 +8,25 @@ export default defineConfig({
8
  appType: 'mpa',
9
  server: {
10
  host: '0.0.0.0', // Listen on all available network interfaces
11
- port: 3000,
12
  headers: {
13
  'Cross-Origin-Embedder-Policy': 'require-corp',
14
- 'Cross-Origin-Opener-Policy': 'same-origin'
15
  },
 
 
 
 
 
 
 
 
 
 
 
16
  proxy: {
17
  '/api/aia': {
18
- target: 'http://aia:4000',
19
  changeOrigin: true,
20
  rewrite: (path) => {
21
  console.log('Rewriting path:', path);
 
8
  appType: 'mpa',
9
  server: {
10
  host: '0.0.0.0', // Listen on all available network interfaces
11
+ port: 7860,
12
  headers: {
13
  'Cross-Origin-Embedder-Policy': 'require-corp',
14
+ 'Cross-Origin-Opener-Policy': 'same-origin',
15
  },
16
+ middleware: [
17
+ // Add custom middleware for /dummy route
18
+ (req, res, next) => {
19
+ if (req.url === '/dummy') {
20
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
21
+ res.end('Hello World!');
22
+ return;
23
+ }
24
+ next();
25
+ }
26
+ ],
27
  proxy: {
28
  '/api/aia': {
29
+ target: 'http://127.0.0.1:4000',
30
  changeOrigin: true,
31
  rewrite: (path) => {
32
  console.log('Rewriting path:', path);