linguabot commited on
Commit
19ad0d0
·
verified ·
1 Parent(s): 1dfd707

Upload folder using huggingface_hub

Browse files
client/src/components/Refinity.tsx ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ type Stage = 'task' | 'flow' | 'editor';
4
+
5
+ type Version = {
6
+ id: string;
7
+ taskId: string;
8
+ originalAuthor: string;
9
+ revisedBy?: string;
10
+ versionNumber: number; // 1-based
11
+ content: string; // translated text
12
+ parentVersionId?: string; // lineage
13
+ };
14
+
15
+ type Task = {
16
+ id: string;
17
+ title: string;
18
+ sourceText: string;
19
+ };
20
+
21
+ const mockTasks: Task[] = [
22
+ { id: 't1', title: 'Refinity Demo Task 1', sourceText: 'The quick brown fox jumps over the lazy dog.' },
23
+ { id: 't2', title: 'Refinity Demo Task 2', sourceText: 'To be, or not to be, that is the question.' },
24
+ ];
25
+
26
+ const Refinity: React.FC = () => {
27
+ const [stage, setStage] = React.useState<Stage>('task');
28
+ const [tasks] = React.useState<Task[]>(mockTasks);
29
+ const [selectedTaskId, setSelectedTaskId] = React.useState<string>(mockTasks[0]?.id || '');
30
+ const [versions, setVersions] = React.useState<Version[]>([]);
31
+ const [currentVersionId, setCurrentVersionId] = React.useState<string | null>(null);
32
+ const [username, setUsername] = React.useState<string>(() => {
33
+ try {
34
+ const u = localStorage.getItem('user');
35
+ const name = u ? (JSON.parse(u)?.name || JSON.parse(u)?.email || 'Anonymous') : 'Anonymous';
36
+ return String(name);
37
+ } catch {
38
+ return 'Anonymous';
39
+ }
40
+ });
41
+
42
+ // File upload (.docx placeholder)
43
+ const [uploading, setUploading] = React.useState(false);
44
+
45
+ const task = React.useMemo(() => tasks.find(t => t.id === selectedTaskId) || tasks[0], [tasks, selectedTaskId]);
46
+ const taskVersions = React.useMemo(() => versions.filter(v => v.taskId === (task?.id || '')), [versions, task?.id]);
47
+
48
+ const focusedIndex = React.useMemo(() => {
49
+ const idx = taskVersions.findIndex(v => v.id === currentVersionId);
50
+ return idx >= 0 ? idx : (taskVersions.length ? taskVersions.length - 1 : -1);
51
+ }, [taskVersions, currentVersionId]);
52
+
53
+ const uploadDocx = async (file: File) => {
54
+ setUploading(true);
55
+ try {
56
+ // Placeholder: In a real implementation, parse .docx client-side or via backend
57
+ // For now, create a mock text from filename
58
+ const baseText = `Uploaded translation from ${username}: ${file.name}`;
59
+ const nextVersionNumber = (taskVersions[taskVersions.length - 1]?.versionNumber || 0) + 1;
60
+ const newVersion: Version = {
61
+ id: `v_${Date.now()}`,
62
+ taskId: task?.id || 't1',
63
+ originalAuthor: username,
64
+ revisedBy: undefined,
65
+ versionNumber: nextVersionNumber,
66
+ content: baseText,
67
+ };
68
+ setVersions(prev => [...prev, newVersion]);
69
+ setCurrentVersionId(newVersion.id);
70
+ setStage('flow');
71
+ } finally {
72
+ setUploading(false);
73
+ }
74
+ };
75
+
76
+ const assignRandom = () => {
77
+ const pool = taskVersions.filter(v => (v.originalAuthor !== username && v.revisedBy !== username));
78
+ if (!pool.length) return;
79
+ const pick = pool[Math.floor(Math.random() * pool.length)];
80
+ setCurrentVersionId(pick.id);
81
+ setStage('editor');
82
+ };
83
+
84
+ const selectManual = (id: string) => {
85
+ setCurrentVersionId(id);
86
+ setStage('editor');
87
+ };
88
+
89
+ const handleSaveRevision = (newContent: string) => {
90
+ const parent = taskVersions.find(v => v.id === currentVersionId);
91
+ const nextVersionNumber = (taskVersions[taskVersions.length - 1]?.versionNumber || 0) + 1;
92
+ const v: Version = {
93
+ id: `v_${Date.now()}`,
94
+ taskId: task?.id || 't1',
95
+ originalAuthor: parent?.originalAuthor || username,
96
+ revisedBy: username,
97
+ versionNumber: nextVersionNumber,
98
+ content: newContent,
99
+ parentVersionId: parent?.id,
100
+ };
101
+ setVersions(prev => [...prev, v]);
102
+ setCurrentVersionId(v.id);
103
+ setStage('flow');
104
+ };
105
+
106
+ return (
107
+ <div className="bg-slate-900 text-slate-50">
108
+ {/* Stage 1: Task Creation / Selection */}
109
+ {stage === 'task' && (
110
+ <div className="p-6">
111
+ <div className="mb-6">
112
+ <h2 className="text-2xl font-semibold">Refinity</h2>
113
+ <p className="text-slate-300">Infinite peer-based translation revision</p>
114
+ </div>
115
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 items-end">
116
+ <div className="md:col-span-1">
117
+ <label className="block text-sm text-slate-300 mb-1">Your name</label>
118
+ <input value={username} onChange={(e)=>setUsername(e.target.value)} className="w-full px-3 py-2 rounded-md bg-slate-800 border border-slate-700 text-slate-100" />
119
+ </div>
120
+ <div className="md:col-span-1">
121
+ <label className="block text-sm text-slate-300 mb-1">Task</label>
122
+ <select value={selectedTaskId} onChange={(e)=>setSelectedTaskId(e.target.value)} className="w-full px-3 py-2 rounded-md bg-slate-800 border border-slate-700 text-slate-100">
123
+ {tasks.map(t => <option key={t.id} value={t.id}>{t.title}</option>)}
124
+ </select>
125
+ </div>
126
+ <div className="md:col-span-1">
127
+ <label className="block text-sm text-slate-300 mb-1">Upload .docx translation</label>
128
+ <input type="file" accept=".docx" onChange={(e)=>{ const f=e.target.files?.[0]; if(f) uploadDocx(f); }} className="block w-full text-slate-200 file:px-3 file:py-2 file:rounded-md file:border-0 file:bg-indigo-600 file:text-white hover:file:bg-indigo-700 disabled:file:bg-slate-700" disabled={uploading} />
129
+ </div>
130
+ </div>
131
+
132
+ <div className="mt-8 flex gap-3">
133
+ <button onClick={assignRandom} className="px-4 py-2 rounded-2xl bg-indigo-600 hover:bg-indigo-700">Start Revising (Random)</button>
134
+ <button onClick={()=>setStage('flow')} className="px-4 py-2 rounded-2xl bg-slate-800 border border-slate-700">Choose Manually</button>
135
+ </div>
136
+ </div>
137
+ )}
138
+
139
+ {/* Stage 2: Version Flow */}
140
+ {stage === 'flow' && (
141
+ <div className="p-6">
142
+ <div className="flex items-center justify-between mb-4">
143
+ <div>
144
+ <h3 className="text-xl font-semibold">Version Flow — {task?.title}</h3>
145
+ <div className="text-slate-400 text-sm">Scroll to browse. Center card is focused.</div>
146
+ </div>
147
+ <div className="flex gap-2">
148
+ <button onClick={()=>setStage('task')} className="px-3 py-2 rounded-md bg-slate-800 border border-slate-700">Back</button>
149
+ </div>
150
+ </div>
151
+
152
+ <div className="relative overflow-x-auto py-10">
153
+ <div className="flex items-center gap-8 min-w-max px-6">
154
+ {taskVersions.map((v, idx) => {
155
+ const isCenter = idx === focusedIndex;
156
+ const sideOffset = Math.abs(idx - focusedIndex);
157
+ const scale = isCenter ? 1.0 : Math.max(0.8, 1 - sideOffset * 0.08);
158
+ const rotate = isCenter ? 0 : (idx < focusedIndex ? -12 : 12);
159
+ const opacity = isCenter ? 1 : Math.max(0.35, 1 - sideOffset * 0.2);
160
+ return (
161
+ <div key={v.id} className="transition-transform duration-300"
162
+ style={{ transform: `scale(${scale}) rotateY(${rotate}deg)`, opacity }}>
163
+ <div className={`w-[520px] bg-slate-800 border border-slate-700 rounded-xl shadow-xl p-5`}
164
+ onClick={()=>setCurrentVersionId(v.id)}>
165
+ <div className="text-slate-200 text-sm mb-2">Original: {v.originalAuthor}</div>
166
+ <div className="text-slate-400 text-xs mb-3">Revised by: {v.revisedBy ? `${v.revisedBy} (v${v.versionNumber})` : `— (v${v.versionNumber})`}</div>
167
+ <div className="text-slate-100 whitespace-pre-wrap break-words min-h-[160px]">{v.content}</div>
168
+ {isCenter && (
169
+ <div className="mt-4 flex gap-3">
170
+ <button className="px-3 py-2 rounded-md bg-slate-700 border border-slate-600">Compare</button>
171
+ <button className="px-3 py-2 rounded-md bg-slate-700 border border-slate-600">Comment</button>
172
+ <button onClick={()=>selectManual(v.id)} className="px-3 py-2 rounded-md bg-indigo-600">Edit</button>
173
+ </div>
174
+ )}
175
+ </div>
176
+ </div>
177
+ );
178
+ })}
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )}
183
+
184
+ {/* Stage 3: Editor */}
185
+ {stage === 'editor' && (
186
+ <EditorPane
187
+ source={task?.sourceText || ''}
188
+ initialTranslation={taskVersions.find(v => v.id === currentVersionId)?.content || ''}
189
+ onBack={()=>setStage('flow')}
190
+ onSave={handleSaveRevision}
191
+ />
192
+ )}
193
+ </div>
194
+ );
195
+ };
196
+
197
+ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack: ()=>void; onSave: (text: string)=>void }>=({ source, initialTranslation, onBack, onSave })=>{
198
+ const [text, setText] = React.useState<string>(initialTranslation);
199
+ const [saving, setSaving] = React.useState(false);
200
+
201
+ const save = async ()=>{
202
+ setSaving(true);
203
+ try {
204
+ onSave(text);
205
+ } finally {
206
+ setSaving(false);
207
+ }
208
+ };
209
+
210
+ const downloadWithTrackChanges = async ()=>{
211
+ // Placeholder: Implement docx generation with track changes via backend or client lib
212
+ alert('Download with Track Changes is a placeholder in this prototype.');
213
+ };
214
+
215
+ return (
216
+ <div className="p-6">
217
+ <div className="flex items-start gap-6">
218
+ <div className="w-1/2">
219
+ <div className="mb-2 text-slate-300 text-sm">Source</div>
220
+ <div className="rounded-lg bg-slate-800 border border-slate-700 p-4 min-h-[420px] whitespace-pre-wrap">{source}</div>
221
+ </div>
222
+ <div className="w-1/2">
223
+ <div className="mb-2 text-slate-300 text-sm">Translation</div>
224
+ <textarea value={text} onChange={(e)=>setText(e.target.value)} className="w-full h-[420px] rounded-lg bg-slate-900 border border-slate-700 p-4 text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-600" />
225
+ <div className="mt-4 flex gap-3">
226
+ <button onClick={save} disabled={saving} className="px-4 py-2 rounded-2xl bg-indigo-600 disabled:bg-slate-700">{saving? 'Saving…':'Save'}</button>
227
+ <button className="px-4 py-2 rounded-2xl bg-slate-800 border border-slate-700">Compare</button>
228
+ <button onClick={downloadWithTrackChanges} className="px-4 py-2 rounded-2xl bg-slate-800 border border-slate-700">Download with Track Changes</button>
229
+ <button onClick={onBack} className="ml-auto px-4 py-2 rounded-2xl bg-slate-800 border border-slate-700">Back</button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ );
235
+ };
236
+
237
+ export default Refinity;
238
+
239
+
client/src/pages/Toolkit.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import SyntaxReorderer from '../components/SyntaxReorderer';
3
  import { api } from '../services/api';
4
  import {
5
  DocumentTextIcon,
@@ -9,7 +9,7 @@ import {
9
  ArrowsRightLeftIcon
10
  } from '@heroicons/react/24/outline';
11
 
12
- type ToolKey = 'quality-lens' | 'mymemory' | 'dictionary' | 'syntax-reorderer' | 'mt' | 'links';
13
 
14
  interface MyMemoryResponse {
15
  responseData?: {
@@ -50,7 +50,7 @@ const TOOL_URLS: Record<ToolKey, string> = {
50
  'quality-lens': 'https://linguabot-quality-lens.hf.space',
51
  'mymemory': '',
52
  'dictionary': '',
53
- 'syntax-reorderer': '',
54
  'mt': '',
55
  'links': ''
56
  };
@@ -124,7 +124,7 @@ const Toolkit: React.FC = () => {
124
  { key: 'quality-lens' as ToolKey, name: 'Quality Lens', desc: 'BLASER/COMET QE + Hallucination', type: 'iframe' },
125
  { key: 'mymemory' as ToolKey, name: 'MyMemory', desc: 'Public MT memory lookup', type: 'native' },
126
  { key: 'dictionary' as ToolKey, name: 'Dictionary (EN⇄ZH)', desc: 'Iciba suggest', type: 'native' },
127
- { key: 'syntax-reorderer' as ToolKey, name: 'Syntax Reorderer', desc: 'EN↔ZH structural practice', type: 'native' },
128
  { key: 'links' as ToolKey, name: 'Useful Links', desc: 'Curated external resources', type: 'native' }
129
  ];
130
  if (!isVisitor) base.splice(2, 0, { key: 'mt' as ToolKey, name: 'MT (DeepL/Google)', desc: 'Translate with MT engines', type: 'native' });
@@ -613,19 +613,19 @@ const Toolkit: React.FC = () => {
613
  </div>
614
  )}
615
 
616
- {selectedTool === 'syntax-reorderer' && (
617
  <div>
618
  <div className="flex items-center space-x-3 mb-4">
619
- <div className="bg-amber-600 rounded-lg p-2">
620
- <DocumentTextIcon className="h-5 w-5 text-white" />
621
  </div>
622
  <div>
623
- <h3 className="text-amber-900 font-semibold text-xl">Syntax Reorderer (EN↔ZH)</h3>
624
- <p className="text-gray-600 text-sm">Label chunks, assign roles, and reorder to practice structural shifts.</p>
625
  </div>
626
  </div>
627
  <div className="border rounded-lg overflow-hidden">
628
- <SyntaxReorderer />
629
  </div>
630
  </div>
631
  )}
 
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import Refinity from '../components/Refinity';
3
  import { api } from '../services/api';
4
  import {
5
  DocumentTextIcon,
 
9
  ArrowsRightLeftIcon
10
  } from '@heroicons/react/24/outline';
11
 
12
+ type ToolKey = 'quality-lens' | 'mymemory' | 'dictionary' | 'refinity' | 'mt' | 'links';
13
 
14
  interface MyMemoryResponse {
15
  responseData?: {
 
50
  'quality-lens': 'https://linguabot-quality-lens.hf.space',
51
  'mymemory': '',
52
  'dictionary': '',
53
+ 'refinity': '',
54
  'mt': '',
55
  'links': ''
56
  };
 
124
  { key: 'quality-lens' as ToolKey, name: 'Quality Lens', desc: 'BLASER/COMET QE + Hallucination', type: 'iframe' },
125
  { key: 'mymemory' as ToolKey, name: 'MyMemory', desc: 'Public MT memory lookup', type: 'native' },
126
  { key: 'dictionary' as ToolKey, name: 'Dictionary (EN⇄ZH)', desc: 'Iciba suggest', type: 'native' },
127
+ { key: 'refinity' as ToolKey, name: 'Refinity', desc: 'Infinite peer-based revision', type: 'native' },
128
  { key: 'links' as ToolKey, name: 'Useful Links', desc: 'Curated external resources', type: 'native' }
129
  ];
130
  if (!isVisitor) base.splice(2, 0, { key: 'mt' as ToolKey, name: 'MT (DeepL/Google)', desc: 'Translate with MT engines', type: 'native' });
 
613
  </div>
614
  )}
615
 
616
+ {selectedTool === 'refinity' && (
617
  <div>
618
  <div className="flex items-center space-x-3 mb-4">
619
+ <div className="bg-indigo-600 rounded-lg p-2">
620
+ <WrenchScrewdriverIcon className="h-5 w-5 text-white" />
621
  </div>
622
  <div>
623
+ <h3 className="text-indigo-900 font-semibold text-xl">Refinity</h3>
624
+ <p className="text-gray-600 text-sm">An elegant, collaborative, infinite revision platform.</p>
625
  </div>
626
  </div>
627
  <div className="border rounded-lg overflow-hidden">
628
+ <Refinity />
629
  </div>
630
  </div>
631
  )}