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

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. client/src/components/Refinity.tsx +119 -41
client/src/components/Refinity.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import React from 'react';
 
2
 
3
  type Stage = 'task' | 'flow' | 'editor';
4
 
@@ -29,7 +30,7 @@ const Refinity: React.FC = () => {
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';
@@ -53,9 +54,13 @@ const Refinity: React.FC = () => {
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()}`,
@@ -104,48 +109,65 @@ const Refinity: React.FC = () => {
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
 
@@ -160,16 +182,28 @@ const Refinity: React.FC = () => {
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>
@@ -190,6 +224,8 @@ const Refinity: React.FC = () => {
190
  onSave={handleSaveRevision}
191
  />
192
  )}
 
 
193
  </div>
194
  );
195
  };
@@ -197,6 +233,8 @@ const Refinity: React.FC = () => {
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);
@@ -207,27 +245,67 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
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>
 
1
  import React from 'react';
2
+ import { api } from '../services/api';
3
 
4
  type Stage = 'task' | 'flow' | 'editor';
5
 
 
30
  const [selectedTaskId, setSelectedTaskId] = React.useState<string>(mockTasks[0]?.id || '');
31
  const [versions, setVersions] = React.useState<Version[]>([]);
32
  const [currentVersionId, setCurrentVersionId] = React.useState<string | null>(null);
33
+ const [username] = React.useState<string>(() => {
34
  try {
35
  const u = localStorage.getItem('user');
36
  const name = u ? (JSON.parse(u)?.name || JSON.parse(u)?.email || 'Anonymous') : 'Anonymous';
 
54
  const uploadDocx = async (file: File) => {
55
  setUploading(true);
56
  try {
57
+ const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
58
+ const form = new FormData();
59
+ form.append('file', file);
60
+ const resp = await fetch(`${base}/api/refinity/parse`, { method: 'POST', body: form });
61
+ const data = await resp.json().catch(()=>({}));
62
+ if (!resp.ok) throw new Error(data?.error || 'Failed to parse document');
63
+ const baseText = String(data?.text || '').trim() || `Uploaded translation by ${username}`;
64
  const nextVersionNumber = (taskVersions[taskVersions.length - 1]?.versionNumber || 0) + 1;
65
  const newVersion: Version = {
66
  id: `v_${Date.now()}`,
 
109
  };
110
 
111
  return (
112
+ <div className="relative">
113
+ {/* Indigo-tinted gradient underlay */}
114
+ <div className="absolute inset-0 rounded-xl bg-gradient-to-r from-indigo-200/45 via-indigo-100/40 to-indigo-300/45" />
115
+ {/* Glass card container */}
116
+ <div className="relative rounded-xl bg-white/10 backdrop-blur-md ring-1 ring-inset ring-white/30 shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)]">
117
+ <div className="pointer-events-none absolute inset-0 rounded-xl opacity-50 [background:linear-gradient(to_bottom,rgba(255,255,255,0.3),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.28),rgba(255,255,255,0)_28%)]" />
118
+ <div className="pointer-events-none absolute inset-0 rounded-xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-45" />
119
+ <div className="pointer-events-none absolute inset-0 rounded-xl" style={{ background: 'radial-gradient(120% 120% at 50% 55%, rgba(0,0,0,0.03), rgba(0,0,0,0) 60%)' }} />
120
+ <div className="pointer-events-none absolute inset-0 rounded-xl bg-indigo-500/10 mix-blend-overlay opacity-30" />
121
+ <div className="relative p-6">
122
  {/* Stage 1: Task Creation / Selection */}
123
  {stage === 'task' && (
124
+ <div>
125
  <div className="mb-6">
126
+ <h2 className="text-2xl font-semibold text-black">Refinity</h2>
127
+ <p className="text-gray-700">Infinite peer-based translation revision</p>
128
  </div>
129
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6 items-end">
130
  <div className="md:col-span-1">
131
+ <label className="block text-sm text-gray-700 mb-1">Task</label>
132
+ <select value={selectedTaskId} onChange={(e)=>setSelectedTaskId(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-white/50 backdrop-blur border border-ui-border text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
 
 
 
 
133
  {tasks.map(t => <option key={t.id} value={t.id}>{t.title}</option>)}
134
  </select>
135
  </div>
136
+ <div className="md:col-span-2">
137
+ <label className="block text-sm text-gray-700 mb-1">Upload .doc or .docx translation</label>
138
+ <input type="file" accept=".doc,.docx" onChange={(e)=>{ const f=e.target.files?.[0]; if(f) uploadDocx(f); }} className="block w-full text-gray-800 file:px-4 file:py-2 file:rounded-2xl file:ring-1 file:ring-inset file:ring-white/50 file:backdrop-blur-md file:backdrop-brightness-110 file:backdrop-saturate-150 file:bg-indigo-600/70 file:text-white hover:file:opacity-95 disabled:file:bg-gray-400" disabled={uploading} />
139
  </div>
140
  </div>
141
 
142
  <div className="mt-8 flex gap-3">
143
+ <button onClick={assignRandom} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-indigo-600/70 active:translate-y-0.5 transition-all duration-200">
144
+ <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
145
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
146
+ <span className="relative z-10">Start Revising (Random)</span>
147
+ </button>
148
+ <button onClick={()=>setStage('flow')} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">
149
+ <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
150
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
151
+ <span className="relative z-10">Choose Manually</span>
152
+ </button>
153
  </div>
154
  </div>
155
  )}
156
 
157
  {/* Stage 2: Version Flow */}
158
  {stage === 'flow' && (
159
+ <div>
160
  <div className="flex items-center justify-between mb-4">
161
  <div>
162
+ <h3 className="text-xl font-semibold text-black">Version Flow — {task?.title}</h3>
163
+ <div className="text-gray-700 text-sm">Scroll to browse. Center card is focused.</div>
164
  </div>
165
  <div className="flex gap-2">
166
+ <button onClick={()=>setStage('task')} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">
167
+ <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
168
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
169
+ <span className="relative z-10">Back</span>
170
+ </button>
171
  </div>
172
  </div>
173
 
 
182
  return (
183
  <div key={v.id} className="transition-transform duration-300"
184
  style={{ transform: `scale(${scale}) rotateY(${rotate}deg)`, opacity }}>
185
+ <div className="relative w-[520px] rounded-xl p-5 bg-white/10 backdrop-blur-md ring-1 ring-inset ring-white/30 shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)]"
186
  onClick={()=>setCurrentVersionId(v.id)}>
187
+ <div className="text-gray-800 text-sm mb-2">Original: {v.originalAuthor}</div>
188
+ <div className="text-gray-600 text-xs mb-3">Revised by: {v.revisedBy ? `${v.revisedBy} (v${v.versionNumber})` : `— (v${v.versionNumber})`}</div>
189
+ <div className="text-gray-900 whitespace-pre-wrap break-words min-h-[160px]">{v.content}</div>
190
  {isCenter && (
191
  <div className="mt-4 flex gap-3">
192
+ <button className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">
193
+ <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
194
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
195
+ <span className="relative z-10">Compare</span>
196
+ </button>
197
+ <button className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">
198
+ <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
199
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
200
+ <span className="relative z-10">Comment</span>
201
+ </button>
202
+ <button onClick={()=>selectManual(v.id)} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-indigo-600/70 active:translate-y-0.5 transition-all duration-200">
203
+ <div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
204
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
205
+ <span className="relative z-10">Edit</span>
206
+ </button>
207
  </div>
208
  )}
209
  </div>
 
224
  onSave={handleSaveRevision}
225
  />
226
  )}
227
+ </div>
228
+ </div>
229
  </div>
230
  );
231
  };
 
233
  const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack: ()=>void; onSave: (text: string)=>void }>=({ source, initialTranslation, onBack, onSave })=>{
234
  const [text, setText] = React.useState<string>(initialTranslation);
235
  const [saving, setSaving] = React.useState(false);
236
+ const [diffHtml, setDiffHtml] = React.useState<string>('');
237
+ const [showDiff, setShowDiff] = React.useState<boolean>(false);
238
 
239
  const save = async ()=>{
240
  setSaving(true);
 
245
  }
246
  };
247
 
248
+ const compareNow = async ()=>{
249
+ try {
250
+ const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
251
+ const resp = await fetch(`${base}/api/refinity/diff`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prev: initialTranslation || '', current: text || '' }) });
252
+ const data = await resp.json().catch(()=>({}));
253
+ if (!resp.ok) throw new Error(data?.error || 'Diff failed');
254
+ setDiffHtml(String(data?.html || ''));
255
+ setShowDiff(true);
256
+ } catch {}
257
+ };
258
+
259
  const downloadWithTrackChanges = async ()=>{
260
+ try {
261
+ const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
262
+ const resp = await fetch(`${base}/api/refinity/track-changes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prev: initialTranslation || '', current: text || '' }) });
263
+ if (!resp.ok) throw new Error('Export failed');
264
+ const blob = await resp.blob();
265
+ const url = window.URL.createObjectURL(blob);
266
+ const a = document.createElement('a');
267
+ a.href = url;
268
+ a.download = 'refinity-tracked.docx';
269
+ document.body.appendChild(a);
270
+ a.click();
271
+ a.remove();
272
+ window.URL.revokeObjectURL(url);
273
+ } catch {
274
+ alert('Export failed');
275
+ }
276
  };
277
 
278
  return (
279
+ <div>
280
  <div className="flex items-start gap-6">
281
  <div className="w-1/2">
282
+ <div className="mb-2 text-gray-700 text-sm">Source</div>
283
+ <div className="relative rounded-lg">
284
+ <div className="absolute inset-0 rounded-lg bg-gradient-to-r from-indigo-200/45 via-indigo-100/40 to-indigo-300/45" />
285
+ <div className="relative rounded-lg bg-white/10 backdrop-blur-md ring-1 ring-inset ring-white/30 shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)] p-4 min-h-[420px] whitespace-pre-wrap text-gray-900">
286
+ {source}
287
+ </div>
288
+ </div>
289
  </div>
290
  <div className="w-1/2">
291
+ <div className="mb-2 text-gray-700 text-sm">Translation</div>
292
+ <textarea value={text} onChange={(e)=>setText(e.target.value)} className="relative z-10 w-full h-[420px] px-4 py-3 border border-ui-border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white" />
293
  <div className="mt-4 flex gap-3">
294
+ <button onClick={save} disabled={saving} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-indigo-600/70 disabled:bg-gray-400 active:translate-y-0.5 transition-all duration-200">{saving? 'Saving…':'Save'}</button>
295
+ <button onClick={compareNow} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">Compare</button>
296
+ <button onClick={downloadWithTrackChanges} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">Download with Track Changes</button>
297
+ <button onClick={onBack} className="ml-auto relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">Back</button>
298
  </div>
299
+ {showDiff && (
300
+ <div className="mt-6 relative rounded-xl">
301
+ <div className="absolute inset-0 rounded-xl bg-gradient-to-r from-indigo-200/45 via-indigo-100/40 to-indigo-300/45" />
302
+ <div className="relative rounded-xl bg-white/10 backdrop-blur-md ring-1 ring-inset ring-white/30 shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)] p-4">
303
+ <div className="pointer-events-none absolute inset-0 rounded-xl opacity-50 [background:linear-gradient(to_bottom,rgba(255,255,255,0.3),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.28),rgba(255,255,255,0)_28%)]" />
304
+ <div className="pointer-events-none absolute inset-0 rounded-xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-45" />
305
+ <div className="relative text-gray-900 prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: diffHtml }} />
306
+ </div>
307
+ </div>
308
+ )}
309
  </div>
310
  </div>
311
  </div>