Alleinzellgaenger commited on
Commit
9e51bdc
·
1 Parent(s): fcb8451

Created PDF upload functionality. A bit rough but works.

Browse files
frontend/ model_view.js DELETED
File without changes
frontend/.DS_Store DELETED
Binary file (6.15 kB)
 
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs['recommended-latest'],
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html CHANGED
@@ -1,131 +1,13 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>DeepGaze</title>
7
- <link rel="stylesheet" href="static/styles.css" />
8
- <!-- Load D3 -->
9
- <script src="https://d3js.org/d3.v6.min.js"></script>
10
- <style>
11
- /* Process button styling */
12
- .text-form button {
13
- background-color: #800000; /* Bordo red */
14
- color: #fff;
15
- border: none;
16
- padding: 8px 12px;
17
- font-size: 14px;
18
- border-radius: 4px;
19
- cursor: pointer;
20
- }
21
- /* Info container styling for head and layer display */
22
- .info-container {
23
- display: flex;
24
- align-items: center;
25
- margin: 5px 0;
26
- font-size: 0.9rem;
27
- }
28
- .info-container .label {
29
- margin-right: 5px;
30
- }
31
- .info-container .number-box {
32
- border: 1px solid #800000;
33
- border-radius: 4px;
34
- padding: 2px 6px;
35
- font-weight: bold;
36
- color: #800000;
37
- min-width: 20px;
38
- text-align: center;
39
- }
40
- </style>
41
- </head>
42
- <body>
43
- <!-- Header -->
44
- <header>
45
- <h1>DeepGaze</h1>
46
- </header>
47
- <!-- Main container -->
48
- <div class="container">
49
- <main>
50
- <!-- About Attention Section -->
51
- <section class="about-section">
52
- <h2>About Attention</h2>
53
- <p>
54
- Transformer networks (the architecture behind Chat-GPT) are built from multiple layers, and each layer is divided into several attention heads.
55
- Each head computes its own attention matrix by combining "queries" and "keys"—the fundamental elements that help
56
- the network decide how much focus to give to different parts of the input.
57
- </p>
58
- <p>
59
- You can think of each query as a question that a token asks, such as "Are there adjectives in front of me?"
60
- Meanwhile, each key serves as a potential answer, carrying the token's characteristics. When the model compares
61
- queries with keys, it determines the strength of their match and, therefore, how much influence one token should
62
- have on another.
63
- </p>
64
- <p>
65
- For example, consider the phrase "fluffy blue monster." One token might generate a query like, "Is the word in front
66
- of me an adjective?" In this case, the tokens "fluffy" and "blue"—which are adjectives—provide keys that answer this
67
- question strongly, while "monster," being a noun, offers a weaker response. This interplay of questions (queries)
68
- and answers (keys) is what creates the attention matrix for each head.
69
- </p>
70
- <p>
71
- Each attention head focuses on different relationships and patterns within the text, allowing the network to capture
72
- a rich and nuanced understanding of the language. Despite the critical role that these attention mechanisms play,
73
- it's interesting to note that only about one third of all the weights in a large language model are actually in the
74
- attention blocks. So while the famous slogan "attention is all you need" highlights the importance of these connections,
75
- in terms of sheer weight, it's only one third of what you really need!
76
- </p>
77
- <p id="credits">Made with <3 by Ferdi & Samu. Credits for model view below to <a href="https://jessevig.com/" target="_blank">BertViz</a>.</p>
78
- </section>
79
-
80
- <!-- Deep Gaze into Attention Heads Section -->
81
- <section class="deep-gaze-section">
82
- <h2>A Deep Gaze into Attention Heads</h2>
83
- <p>Type in a token sequence (below 50 characters) and hit process. After some loading time, you will be able to see the attention patterns of individual so-called "heads" in the LLM.
84
- Each head focuses on different aspects of the input text, and by visualizing these patterns, you can gain insights into how the model processes and understands language.
85
- </p>
86
-
87
- <p>Here is an example view of a head, with tokens on each side. If you see a connection between two tokens, it means that the head is paying attention to the relationship between those tokens. This way you can see attention heads which "pay attention" to the previous token, the first token, or other patterns.
88
- Click on an attention head to select the respective head in a layer. Afterwards you can hover over tokens to see the attention weights of the selected head for that token. </p>
89
- <div id="thumbnailContainer"></div>
90
- <!-- Text Input & Process Button -->
91
- <form id="textForm" class="text-form">
92
- <textarea id="inputText" rows="2" cols="50" maxlength="50" placeholder="Enter your text here..." autofocus></textarea>
93
- <button type="submit">Process</button>
94
- </form>
95
- </section>
96
-
97
- <!-- Model View Section -->
98
- <section class="model_view">
99
- <p>Click on a head that looks interesting to gaze deeper into it in the next section:</p>
100
-
101
- <div id="model_view_container">
102
- <!-- Thumbnails of attention heads will be rendered here -->
103
- </div>
104
- <!-- Display for selected head and layer -->
105
- <div id="display_info">
106
- <div id="display_head" class="info-container">
107
- <span class="label">Head:</span>
108
- <span class="number-box">-</span>
109
- </div>
110
- <div id="display_layer" class="info-container">
111
- <span class="label">Layer:</span>
112
- <span class="number-box">-</span>
113
- </div>
114
- </div>
115
- </section>
116
-
117
- <!-- Hover Visualization Section -->
118
- <section class="hover-visualization">
119
- <h2>Hover Visualization</h2>
120
- <p>
121
- By hovering over each token, you can see which other token is important for that token. The larger the token, the more important it is for the token you are hovering over. The token with the maximal attention is colored in red.
122
- </p>
123
- <div id="tokenContainer"></div>
124
- </section>
125
- </main>
126
- </div>
127
-
128
- <!-- External JavaScript (all event listeners and functions are defined here) -->
129
- <script src="static/script.js?v=1111"></script>
130
- </body>
131
- </html>
 
1
+ <!doctype html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
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>Vite + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@tailwindcss/postcss": "^4.1.11",
14
+ "autoprefixer": "^10.4.21",
15
+ "postcss": "^8.5.6",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1",
18
+ "react-pdf": "^10.0.1",
19
+ "react-router-dom": "^7.7.0",
20
+ "tailwindcss": "^4.1.11"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.30.1",
24
+ "@types/react": "^19.1.8",
25
+ "@types/react-dom": "^19.1.6",
26
+ "@vitejs/plugin-react": "^4.6.0",
27
+ "eslint": "^9.30.1",
28
+ "eslint-plugin-react-hooks": "^5.2.0",
29
+ "eslint-plugin-react-refresh": "^0.4.20",
30
+ "globals": "^16.3.0",
31
+ "vite": "^7.0.4"
32
+ }
33
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/pdf.worker.min.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/public/vite.svg ADDED
frontend/script.js DELETED
@@ -1,213 +0,0 @@
1
- // Form submit handler 2.1
2
- document.getElementById('textForm').addEventListener('submit', async (e) => {
3
- e.preventDefault();
4
- const inputText = document.getElementById('inputText').value;
5
-
6
- try {
7
- const response = await fetch('/process', {
8
- method: 'POST',
9
- headers: { 'Content-Type': 'application/json' },
10
- body: JSON.stringify({ text: inputText })
11
- });
12
-
13
- if (!response.ok) {
14
- throw new Error('Network response was not ok');
15
- }
16
- const data = await response.json();
17
-
18
- // Use data.tokens and data.attention from your POST response
19
- // displayOutput(data);
20
- displayHoverTokens(data, 0, 0);
21
- // Changed call here to pass the entire data object
22
- renderModelView(data);
23
- } catch (error) {
24
- console.error('Error:', error);
25
- document.getElementById('output').innerText = 'Error processing text.';
26
- }
27
- });
28
-
29
- function renderModelView(data) {
30
- // Extract tokens and attention from the data object
31
- const tokens = data.tokens;
32
- const attention = data.attention;
33
-
34
- const container = document.getElementById("model_view_container");
35
- if (!container) return;
36
- container.innerHTML = "";
37
-
38
- const gridContainer = document.createElement("div");
39
- gridContainer.style.display = "grid";
40
- gridContainer.style.gridTemplateColumns = "repeat(12, 80px)";
41
- gridContainer.style.gridGap = "10px";
42
- gridContainer.style.padding = "20px";
43
-
44
- // Loop over all 12 layers and 12 heads, passing the complete data object
45
- for (let layerIdx = 0; layerIdx < 12; layerIdx++) {
46
- for (let headIdx = 0; headIdx < 12; headIdx++) {
47
- const thumbnail = createAttentionThumbnail(data, layerIdx, headIdx);
48
- gridContainer.appendChild(thumbnail);
49
- }
50
- }
51
-
52
- container.appendChild(gridContainer);
53
- }
54
-
55
- function createAttentionThumbnail(data, layerIdx, headIdx) {
56
- // Extract tokens and attention from the data object
57
- const tokens = data.tokens;
58
- const attention = data.attention;
59
-
60
- const width = 80;
61
- const tokenHeight = 15;
62
- const padding = 10;
63
- // Compute the thumbnail height dynamically based on the number of tokens.
64
- const height = padding * 2 + tokens.length * tokenHeight;
65
- const maxLineWidth = 4;
66
- const maxOpacity = 0.8;
67
-
68
- // Compute the right-side x-coordinate numerically.
69
- const xRight = width - padding;
70
-
71
- // Create a thumbnail container using D3.
72
- const thumbnail = d3.select(document.createElement("div"))
73
- .style("position", "relative")
74
- .style("height", height + "px")
75
- .style("width", width + "px")
76
- .style("border", "1px solid #ddd")
77
- .style("border-radius", "4px")
78
- .style("padding", "5px")
79
- .style("background", "#fff");
80
-
81
- // Append an SVG container with fixed dimensions.
82
- const svg = thumbnail.append("svg")
83
- .attr("width", width)
84
- .attr("height", height);
85
-
86
- // Add header text (e.g., "L4 H4") to show the layer and head number.
87
- svg.append("text")
88
- .attr("x", width / 2)
89
- .attr("y", 15)
90
- .attr("text-anchor", "middle")
91
- .attr("font-size", "10")
92
- .text(`L${layerIdx + 1} H${headIdx + 1}`);
93
-
94
- // Draw attention lines with per-row normalization.
95
- attention[layerIdx][headIdx].forEach((sourceWeights, sourceIdx) => {
96
- const rowMax = Math.max(...sourceWeights) || 1;
97
- sourceWeights.forEach((weight, targetIdx) => {
98
- if (weight > 0.01 && sourceIdx !== targetIdx) {
99
- const normalizedWeight = weight / rowMax;
100
- svg.append("line")
101
- .attr("x1", padding)
102
- .attr("y1", padding + sourceIdx * tokenHeight - 5)
103
- .attr("x2", xRight)
104
- .attr("y2", padding + targetIdx * tokenHeight - 5)
105
- .attr("stroke", "#800000") // Bordo red
106
- .attr("stroke-width", Math.max(0.5, normalizedWeight * maxLineWidth))
107
- .attr("opacity", Math.min(maxOpacity, normalizedWeight * 2))
108
- .attr("stroke-linecap", "round");
109
- }
110
- });
111
- });
112
-
113
- // Click handler remains unchanged, using the passed-in data object.
114
- thumbnail.on("click", function() {
115
- d3.select("#display_head .number-box").text(headIdx + 1);
116
- d3.select("#display_layer .number-box").text(layerIdx + 1);
117
- displayHoverTokens(data, layerIdx, headIdx);
118
- });
119
-
120
- return thumbnail.node();
121
- }
122
-
123
- // Function to display the tokens and attention values
124
- // function displayOutput(data) {
125
- // const outputDiv = document.getElementById('output');
126
- // outputDiv.innerHTML = `
127
- // <h2>Tokens</h2>
128
- // <pre>${JSON.stringify(data.tokens, null, 2)}</pre>
129
- // <h2>Attention</h2>
130
- // <pre>${JSON.stringify(data.attention, null, 2)}</pre>
131
- // `;
132
- // }
133
-
134
- function renderTokens(tokens, attentionData, layer_idx, head_idx) {
135
- const container = document.getElementById('tokenContainer');
136
- container.innerHTML = "";
137
-
138
- tokens.forEach((token, index) => {
139
- const span = document.createElement('span');
140
- span.textContent = token.replace("Ġ", "") + " ";
141
- span.style.fontSize = "32px";
142
- span.addEventListener('mouseenter', () => {
143
- highlightAttention(index, attentionData, layer_idx, head_idx);
144
- });
145
- span.addEventListener('mouseleave', () => {
146
- resetTokenSizes();
147
- });
148
- container.appendChild(span);
149
- });
150
- }
151
-
152
- function displayHoverTokens(data, layer_idx, head_idx) {
153
- let tokens, attentionMatrix;
154
- if (!data.tokens || !data.attention) {
155
- tokens = ['This', 'is', 'a', 'test', '.'];
156
- // Create a dummy attention matrix if missing
157
- attentionMatrix = Array(12)
158
- .fill(null)
159
- .map(() => Array(12).fill(null).map(() => Array(tokens.length).fill(0)));
160
- } else {
161
- tokens = data.tokens;
162
- attentionMatrix = data.attention;
163
- }
164
- renderTokens(tokens, attentionMatrix, layer_idx, head_idx);
165
- }
166
-
167
- function resetTokenSizes() {
168
- const container = document.getElementById("tokenContainer");
169
- Array.from(container.children).forEach((span) => {
170
- span.style.fontSize = "32px";
171
- span.style.color = "#555";
172
- });
173
- }
174
-
175
- function highlightAttention(index, attentionData, layer_idx, head_idx) {
176
- const container = document.getElementById('tokenContainer');
177
- const row = attentionData[layer_idx][head_idx][index];
178
-
179
- if (!row) {
180
- console.warn(`No attention data for token index ${index}`);
181
- return;
182
- }
183
- const weights = row;
184
- if (!weights.length) {
185
- return;
186
- }
187
-
188
- // Find the maximum weight
189
- const maxWeight = Math.max(...weights) || 1;
190
- const baseFontSize = 32;
191
- const maxIncrease = 20;
192
-
193
- const maxIndex = weights.indexOf(maxWeight);
194
-
195
- Array.from(container.children).forEach((span, idx) => {
196
- const weight = weights[idx];
197
-
198
- if (typeof weight === 'number') {
199
- const newFontSize = baseFontSize + (weight / maxWeight) * maxIncrease;
200
- span.style.fontSize = newFontSize + "px";
201
-
202
- if (idx === maxIndex) {
203
- span.style.color = "#800000"; // Bordo red
204
- } else {
205
- span.style.color = "#555"; // Reset color
206
- }
207
- } else {
208
- // For tokens without a corresponding weight, reset styles.
209
- span.style.fontSize = baseFontSize + "px";
210
- span.style.color = "#555";
211
- }
212
- });
213
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
2
+ import Homepage from './components/Homepage';
3
+ import UploadPage from './components/UploadPage';
4
+
5
+ function App() {
6
+ return (
7
+ <Router>
8
+ <Routes>
9
+ <Route path="/" element={<Homepage />} />
10
+ <Route path="/upload" element={<UploadPage />} />
11
+ </Routes>
12
+ </Router>
13
+ );
14
+ }
15
+
16
+ export default App;
frontend/src/assets/react.svg ADDED
frontend/src/components/Homepage.jsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Homepage() {
2
+ return (
3
+ <div className="min-h-screen bg-gray-50 flex items-center justify-center">
4
+ <div className="text-center">
5
+ <h1 className="text-4xl font-bold text-gray-900 mb-4">
6
+ Hello World
7
+ </h1>
8
+ <p className="text-lg text-gray-600">
9
+ PDF Interrogation App - Coming Soon
10
+ </p>
11
+ </div>
12
+ </div>
13
+ );
14
+ }
15
+
16
+ export default Homepage;
frontend/src/components/UploadPage.jsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from 'react';
2
+ import { Document, Page, pdfjs } from 'react-pdf';
3
+ import 'react-pdf/dist/Page/AnnotationLayer.css';
4
+ import 'react-pdf/dist/Page/TextLayer.css';
5
+
6
+ pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
7
+
8
+ function UploadPage() {
9
+ const fileInputRef = useRef(null);
10
+ const [selectedFile, setSelectedFile] = useState(null);
11
+ const [numPages, setNumPages] = useState(null);
12
+
13
+ return (
14
+ <div className="min-h-screen bg-gray-50">
15
+ {!selectedFile ? (
16
+ // Show upload UI
17
+ <div className="flex items-center justify-center min-h-screen">
18
+ <div className="text-center">
19
+ <h1 className="text-3xl font-bold text-gray-900 mb-4">
20
+ Upload Your PDF
21
+ </h1>
22
+ <p className="text-gray-600 mb-8">
23
+ Click below to upload a PDF and start your deep dive.
24
+ </p>
25
+ <input
26
+ ref={fileInputRef}
27
+ type="file"
28
+ accept=".pdf"
29
+ className="hidden"
30
+ onChange={(e) => setSelectedFile(e.target.files[0])}
31
+ />
32
+ <button
33
+ onClick={() => fileInputRef.current.click()}
34
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
35
+ >
36
+ Upload PDF
37
+ </button>
38
+ </div>
39
+ </div>
40
+ ) : (
41
+ // Show PDF + chat layout
42
+ <div className="flex min-h-screen">
43
+ <div className="w-2/3 bg-white p-4">
44
+ <Document
45
+ file={selectedFile}
46
+ onLoadSuccess={({ numPages }) => setNumPages(numPages)}
47
+ >
48
+ <Page pageNumber={1} width={600} />
49
+ </Document>
50
+ {numPages && <p className="mt-2">Page 1 of {numPages}</p>}
51
+ </div>
52
+ <div className="w-1/3 bg-gray-100">
53
+ Chat will go here
54
+ </div>
55
+ </div>
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export default UploadPage;
frontend/src/index.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import "tailwindcss";
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/styles.css DELETED
@@ -1,198 +0,0 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
2
- @import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&display=swap');
3
-
4
- *,
5
- *::before,
6
- *::after {
7
- box-sizing: border-box;
8
- }
9
-
10
- /* Basic Resets & Body */
11
- html, body {
12
- margin: 0;
13
- padding: 0;
14
- font-family: 'Lora', 'Roboto', sans-serif;
15
- background-color: #fefdf6; /* Off-white background */
16
- color: #333;
17
- }
18
- input:focus,
19
- select:focus,
20
- textarea:focus,
21
- button:focus {
22
- outline: none;
23
- }
24
- /* Header spanning full width */
25
- header {
26
- width: 100%;
27
- background-color: #fff;
28
- padding: 1rem 2rem;
29
- border-bottom: 1px solid #ddd;
30
- }
31
-
32
- header h1 {
33
- margin: 0;
34
- font-size: 1.8rem;
35
- font-weight: 100;
36
- }
37
-
38
- /*
39
- Container that splits the page:
40
- left (main) ~75%, right (aside) ~25%
41
- */
42
- .container {
43
- display: flex;
44
- flex-direction: row;
45
- min-height: calc(100vh - 60px); /* Keep some height below header */
46
- }
47
-
48
- /* Main content on the left */
49
- .container main {
50
- width: 75%;
51
- padding: 2rem;
52
- box-sizing: border-box;
53
- }
54
-
55
- /* The aside on the right can be used or left empty */
56
- .container aside {
57
- width: 25%;
58
- padding: 2rem;
59
- box-sizing: border-box;
60
- background-color: #fffff8;
61
- }
62
-
63
- /* ABOUT / DEEP GAZE SECTIONS */
64
- .about-section {
65
- margin-bottom: 2rem;
66
- }
67
-
68
- .about-section h2 {
69
- font-size: 1.4rem;
70
- margin-bottom: 1rem;
71
- font-weight: 100;
72
- font-style: italic;
73
-
74
- }
75
-
76
- .about-section p {
77
- line-height: 1.6;
78
- margin-bottom: 1rem;
79
- }
80
-
81
- .deep-gaze-section h2 {
82
- font-size: 1.4rem;
83
- margin-bottom: 0.5rem;
84
- font-weight: 100;
85
- font-style: italic;
86
-
87
- }
88
-
89
- .deep-gaze-section p {
90
- margin-bottom: 1rem;
91
- line-height: 1.6;
92
- }
93
-
94
- /* Selection & Text Form Styles */
95
- .selection-form,
96
- .text-form {
97
- margin-bottom: 1.5rem;
98
- }
99
-
100
- .selection-form label {
101
- margin-right: 0.5rem;
102
- }
103
-
104
- .selection-form select {
105
- margin-right: 1rem;
106
- }
107
-
108
- .text-form {
109
- display: flex;
110
- flex-direction: column;
111
- align-items: flex-start;
112
- border: none;
113
- }
114
-
115
- textarea {
116
- width: 100%;
117
- max-width: 100%;
118
- font-size: 2rem;
119
- font-weight: 700;
120
- margin-bottom: 2rem;
121
- border: none;
122
- resize: none;
123
- font-family: 'Roboto', sans-serif;
124
- background-color: #fffff8;
125
- color: #3a3939;
126
- border-radius: 8px;
127
- padding: 20px;
128
- filter: drop-shadow(0 0 0.75rem #ddd);
129
- }
130
-
131
-
132
- textarea::placeholder {
133
- color: #888;
134
- font-family: 'Roboto', sans-serif;
135
- font-size:2rem;
136
- font-weight: 700;
137
-
138
- }
139
-
140
- button {
141
- background-color: #4b80f9;
142
- color: white;
143
- border: none;
144
- border-radius: 4px;
145
- padding: 0.6rem 1rem;
146
- cursor: pointer;
147
- font-size: 1rem;
148
- }
149
- button:hover {
150
- background-color: #3f6ddb;
151
- }
152
-
153
- /* Output Area */
154
- #output {
155
- background-color: #fff;
156
- border: 1px solid #ddd;
157
- padding: 1rem;
158
- margin-bottom: 1rem;
159
- max-height: 300px;
160
- overflow-y: auto;
161
- }
162
-
163
- #output h2 {
164
- margin-top: 0;
165
- font-size: 1.2rem;
166
- }
167
-
168
- /* Token Container */
169
- #tokenContainer {
170
- margin-top: 1rem;
171
- }
172
-
173
- /* Token highlighting styles */
174
- #tokenContainer span {
175
- cursor: default;
176
- transition: font-size 0.9s ease;
177
- color: #555;
178
- display: inline-block;
179
- transition: font-size 0.9s ease;
180
- margin-right: 4px;
181
- padding: 2px 4px;
182
- }
183
-
184
- .text-form button {
185
- background-color: #800000; /* Bordo red */
186
- color: #fff; /* Optional: set text color to white for contrast */
187
- border: none;
188
- padding: 10px 20px;
189
- font-size: 16px;
190
- cursor: pointer;
191
- }
192
-
193
- #credits {
194
- font-size: 0.8rem;
195
- color: #888;
196
- margin-top: 1rem;
197
- font-style: italic;
198
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })