Spaces:
Sleeping
Sleeping
Particulars of Claim: one button from the uploaded documents (auto-detected parties, court names, claims; [CONFIRM] gaps; check-list page). Old form hidden; /draft_poc, /draft_poc_json, /extract_facts kept
7a277fe verified Download app.py from Basementup/Seamstressesstitching: direct link, hf CLI and curl.
- Browser
- Download file 34.3 kB
-
https://huggingface.co/spaces/Basementup/Seamstressesstitching/resolve/main/app.py
- Command line
-
hf download hf://spaces/Basementup/Seamstressesstitching/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Basementup/Seamstressesstitching/resolve/main/app.py
34.3 kB
| """The Seamstresses - upload every version of your documents (any type), get back ONE sourced case summary | |
| and ONE clean, deduplicated master document. Runs on free CPU, no paid APIs. Reading: intake.py (OCR and | |
| speech-to-text run locally); merge engine: stitch.py; case summary: brief.py; Word/PDF: report_export.py.""" | |
| import hashlib, os, re, tempfile | |
| from datetime import datetime | |
| import gradio as gr | |
| import json | |
| import autopoc | |
| import brief | |
| import intake | |
| import poc | |
| import report_export | |
| import stitch | |
| def _paths(uploads): | |
| if uploads is None: | |
| return [] | |
| if not isinstance(uploads, (list, tuple)): | |
| uploads = [uploads] | |
| out = [] | |
| for f in uploads: | |
| if f is None: | |
| continue | |
| p = (f.get('path') or f.get('name')) if isinstance(f, dict) else (f.name if hasattr(f, 'name') else str(f)) | |
| if p: | |
| out.append(p) | |
| return out | |
| def _safe(name): | |
| return re.sub(r'[^A-Za-z0-9 _-]+', '', name).strip().replace(' ', '_')[:60] or 'Merged_Document' | |
| def stitch_documents(uploads, title='', pasted=''): | |
| files = _paths(uploads) | |
| if not files and not (pasted or '').strip(): | |
| return 'Please upload at least one document.', None, None, None, '' | |
| docs, problems = [], [] | |
| for f in files: | |
| try: | |
| paras = stitch.extract(f) | |
| except Exception as e: | |
| problems.append(f'{os.path.basename(f)}: could not be read ({e})'); continue | |
| if not paras: | |
| problems.append(f'{os.path.basename(f)}: no text found (scanned image PDF?)'); continue | |
| docs.append((os.path.basename(f), paras)) | |
| # optional pasted text: several versions can be separated by a line containing only === | |
| for n, chunk in enumerate(re.split(r'\n\s*={3,}\s*\n', pasted or ''), 1): | |
| if chunk.strip(): | |
| docs.append((f'pasted text {n}', stitch.extract_text(chunk))) | |
| if not docs: | |
| return 'None of the files contained readable text.\n' + '\n'.join(problems), None, None, None, '' | |
| title = (title or '').strip() | |
| blocks, st = stitch.merge(docs) | |
| text = stitch.to_text(title, blocks) | |
| outdir = tempfile.mkdtemp() | |
| base = os.path.join(outdir, _safe(title) if title else 'Merged_Document') | |
| docx_path = stitch.export_docx(title, blocks, base + '.docx') | |
| pdf_path = stitch.export_pdf(title, blocks, base + '.pdf') | |
| with open(base + '.txt', 'w', encoding='utf-8') as fh: | |
| fh.write(text) | |
| note = (f"{st['files']} file(s) stitched into one document. " | |
| f"Words in: {st['words_in']:,} -> out: {st['words_out']:,}. " | |
| f"Repeated paragraphs merged: {st['dup_paras']} " | |
| f"(unique details folded in from the weaker copy: {st['folded']}). " | |
| f"Repeated sentences removed: {st['dup_sents']}. Repeated headings/short lines removed: {st['dup_heads']}. " | |
| f"Backbone (fullest upload, its order is kept): {st['backbone']}.") | |
| if st.get('fallback'): | |
| note += (f"\n\n**Safety check:** the full merge did not pass ({st['fallback']}), so you have been given " | |
| f"the fullest upload ({st['backbone']}) with only its own repeated paragraphs removed.") | |
| if problems: | |
| note += '\nSkipped: ' + '; '.join(problems) | |
| return text, docx_path, pdf_path, base + '.txt', note | |
| # ------------------------------------------------------------------ Particulars of Claim (separate feature) | |
| POC_FIELDS = ['court', 'court_detail', 'claim_no', 'claimant_name', 'claimant_pronoun', 'claimant_is_company', | |
| 'claimant_capacity', 'dob', 'address_for_service', 'signed_by', 'defendants', 'defined_terms', | |
| 'goods_term', 'contract_how', 'contract_ref', 'contract_date', 'contract_obligation', 'price', | |
| 'paid_date', 'paid_how', 'delivery_date', 'express_terms', 'facts', 'causes', 'contract_breaches', | |
| 'description', 'description_mismatch', 'purpose', 'purpose_how', 'defects', 'defect_date', | |
| 'rejection_date', 'rejection_how', 'failings', 'service_failings', 'duty_scope', 'incident', | |
| 'cpa_role', 'representations', 'falsity', 'knowledge', 'impairment', 'effects', 'service', 'arising', | |
| 'unfavourable', 'pcp', 'disadvantage', 'adjustments', 'harassment_conduct', 'personal_data', | |
| 'gdpr_breaches', 'losses', 'injuries', 'medical_report', 'aggravated', 'injunction', | |
| 'interest_principal', 'interest_rate', 'interest_from', 'interest_to', 'value', | |
| 'against', 'contract_defendant', 'credit_card', 'psr_transactions', 'psr_defect_how', 'statutory_duty', | |
| 'enrich_amount', 'enrich_how', 'unjust_factor', 'unjust_basis', 'rescission_return'] | |
| def _poc_result(data): | |
| out = tempfile.mkdtemp(prefix='poc_') | |
| r = poc.draft(data, out) | |
| n = len(r['confirm']) | |
| note = (f"**Draft ready.** Items marked [CONFIRM: ...] need your input: **{n}**. " | |
| "The drafting notes file lists them, the source of every fact paragraph and any warnings " | |
| "(court, time limits, attachments). Built from your input only, using fixed templates. No AI and no paid service.") | |
| if r['stray'] or r['bad_cases']: | |
| note += f"\n\n**Validation flagged:** {', '.join(r['stray'] + r['bad_cases'])}" | |
| return r['text'], r['docx'], r['pdf'], r['txt'], r['notes_file'], note | |
| def draft_poc(*vals): | |
| data = dict(zip(POC_FIELDS, vals)) | |
| data['causes'] = list(data.get('causes') or []) | |
| data['claimant_is_company'] = 'yes' if data.get('claimant_is_company') in (True, 'yes', 'Yes', 'true') else 'no' | |
| data = {k: ('' if v is None else v) for k, v in data.items()} | |
| return _poc_result(data) | |
| def draft_poc_json(text): | |
| data = json.loads(text or '{}') | |
| return _poc_result(data) | |
| def extract_poc_facts(uploads, pasted, existing): | |
| rows = poc.extract_facts(_paths(uploads), pasted or '') | |
| if not rows: | |
| return existing or '', 'No dated sentences were found.' | |
| n = len([r for r in rows.split('\n') if r and not r.startswith('#')]) | |
| merged = ((existing or '').rstrip() + '\n' + rows).strip() | |
| return merged, (f'Added {n} dated sentence(s), copied word for word. Review each line: delete what is not a ' | |
| 'material fact, and replace the file name in the source column with your document reference.') | |
| def create_particulars(uploads, links='', pasted='', picks_json='{}'): | |
| """One button: read the same uploads as the Merge tab, merge/deduplicate them, detect parties, dates, sums, | |
| references and the claims the documents support, and draft Particulars of Claim. Gaps stay [CONFIRM: ...].""" | |
| docs, problems = _gather(uploads, links, pasted) | |
| if not docs: | |
| return ('**Nothing to work from yet.** Upload your documents on the **Merge documents** tab (or paste text there), ' | |
| 'then come back and press the button.' + ('\n\n**Problems:** ' + '; '.join(problems) if problems else ''), | |
| None, None, '') | |
| try: | |
| picks = {str(k): int(v) for k, v in json.loads(picks_json or '{}').items()} | |
| except Exception: # noqa | |
| picks = {} | |
| R = brief.build(docs, picks, '', keep_new=not picks) | |
| r, sections = autopoc.create(docs, R, tempfile.mkdtemp(prefix='poc_')) | |
| md = [f"### ✅ Draft ready from {len(docs)} document(s)", | |
| 'The same check list is the first page of the Word and PDF - delete it before filing.'] | |
| for head, items in sections: | |
| if items: | |
| md.append(f'**{head}**\n' + '\n'.join(f'- {x}' for x in items)) | |
| if r['stray'] or r['bad_cases']: | |
| md.append('**Validation flagged:** ' + ', '.join(r['stray'] + r['bad_cases'])) | |
| if problems: | |
| md.append('**Skipped:** ' + '; '.join(problems)) | |
| return '\n\n'.join(md), r['docx'], r['pdf'], r['text'] | |
| # ------------------------------------------------------------------ new flow: upload -> run -> review -> build -> download | |
| MAX_LINKS = 10 | |
| def _gather(uploads, links='', pasted=''): | |
| """Read every upload / link / pasted chunk -> (docs with ids D1.., problems).""" | |
| docs, problems = [], [] | |
| def take(got): | |
| for d in got: | |
| if d['paras']: | |
| docs.append(dict(d)) | |
| else: | |
| problems.append(f"{d['name']}: no readable text found" + (' (' + '; '.join(d['warnings']) + ')' if d.get('warnings') else '')) | |
| for f in _paths(uploads): | |
| try: | |
| take(intake.load(f)) | |
| except Exception as e: # noqa | |
| problems.append(f'{os.path.basename(f)}: could not be read ({e})') | |
| for ln in [x.strip() for x in re.split(r'[\s,]+', links or '') if x.strip()][:MAX_LINKS]: | |
| if not re.match(r'https://', ln): | |
| problems.append(f'{ln[:60]}: only https:// links can be fetched'); continue | |
| try: | |
| take(intake.load(intake.fetch_link(ln))) | |
| except Exception as e: # noqa | |
| problems.append(f'{ln[:60]}: could not be fetched ({e})') | |
| for n, chunk in enumerate(re.split(r'\n\s*={3,}\s*\n', pasted or ''), 1): | |
| if chunk.strip(): | |
| docs.append({'name': f'pasted text {n}', 'paras': stitch.extract_text(chunk), 'pages': [], | |
| 'sha256': hashlib.sha256(chunk.encode('utf-8')).hexdigest(), 'size': len(chunk.encode('utf-8')), | |
| 'kind': 'text', 'extractor': 'pasted text', 'warnings': []}) | |
| for i, d in enumerate(docs, 1): | |
| d['id'] = f'D{i}' | |
| return docs, problems | |
| def _files(R, mode): | |
| outdir = tempfile.mkdtemp(prefix='seam_') | |
| base = os.path.join(outdir, _safe(R['headline'])[:50] or 'Case_summary') | |
| report_export.export_docx(R, base + '.docx') | |
| report_export.export_pdf(R, base + '.pdf') | |
| with open(base + '.txt', 'w', encoding='utf-8') as fh: | |
| fh.write(R['text']) | |
| with open(base + '_audit.json', 'w', encoding='utf-8') as fh: | |
| fh.write(brief.audit_json(R, mode)) | |
| return base + '.docx', base + '.pdf', base + '.txt', base + '_audit.json' | |
| def _master_text(R): | |
| t = R['text'] | |
| i = t.find('PART 2 - MASTER DOCUMENT') | |
| return t[i:] if i >= 0 else t | |
| def _status(R, docs, problems, mode): | |
| d = R['dedup'] | |
| msg = (f"### ✅ Done - {len(docs)} document(s) → one case summary + master document\n" | |
| f"{'⚡ Strongest version of each duplicate chosen for you' if mode == 'auto' else '✋ Built with your choices'}. " | |
| f"{d['copies_removed']} duplicate paragraph(s) merged, {len(R['conflicts'])} contradiction(s) flagged, " | |
| f"{len(R['chronology'])} chronology entries. SHA-256 seal: `{R['seal'][:16]}…`") | |
| if problems: | |
| msg += '\n\n**Skipped:** ' + '; '.join(problems) | |
| return msg | |
| def _empty_result(msg): | |
| return msg, '', '', None, None, None, None | |
| def _run_build(uploads, links, pasted, title, picks, mode): | |
| docs, problems = _gather(uploads, links, pasted) | |
| if not docs: | |
| return _empty_result('**Nothing to read yet.** Upload at least one document, paste text or add a link.' | |
| + ('\n\n**Problems:** ' + '; '.join(problems) if problems else '')) | |
| R = brief.build(docs, picks, (title or '').strip(), keep_new=(mode == 'auto')) | |
| return (_status(R, docs, problems, mode), brief.to_markdown(R), _master_text(R)) + _files(R, mode) | |
| def choose_for_me(uploads, links, pasted, title): | |
| """Auto-pick the strongest version of every duplicate and build straight away.""" | |
| return _run_build(uploads, links, pasted, title, {}, 'auto') | |
| def build_with_picks(uploads, links, pasted, title, picks_json): | |
| """Build using the user's choice per duplicate group: picks_json = {"group id": version index (0-based)}.""" | |
| try: | |
| picks = {str(k): int(v) for k, v in json.loads(picks_json or '{}').items()} | |
| except Exception: # noqa | |
| picks = {} | |
| return _run_build(uploads, links, pasted, title, picks, 'user') | |
| def let_me_choose(uploads, links, pasted): | |
| """Find the duplicate groups that are worded differently, with the best version pre-selected.""" | |
| docs, problems = _gather(uploads, links, pasted) | |
| if not docs: | |
| return ('**Nothing to read yet.** Upload at least one document, paste text or add a link.', [], '{}', gr.update(visible=False)) | |
| backbone = max(range(len(docs)), key=lambda k: stitch._quality(docs[k]['paras'])) | |
| groups = brief.dup_groups(docs, backbone) | |
| loc = brief.Locator(docs) | |
| view = [] | |
| for g in groups: | |
| if len(g['versions']) < 2: | |
| continue | |
| vs = [] | |
| for v in g['versions']: | |
| rs = loc.find(v['text'], limit=4) or [{'doc': docs[di]['id'], 'page': None, 'para': pi + 1} for di, pi in v['occ']] | |
| names = sorted({docs[di]['name'] for di, _ in v['occ']}) | |
| vs.append({'text': brief.clean(v['text']), 'refs': brief.refs(rs, 4), 'files': ', '.join(names)[:120], | |
| 'words': len(v['text'].split())}) | |
| view.append({'id': g['id'], 'best': g['best'], 'differ': g['figures_differ'], 'versions': vs}) | |
| picks = {str(g['id']): g['best'] for g in view} | |
| same = len(groups) - len(view) | |
| msg = (f"### ✋ Your turn - {len(view)} duplicate group(s) worded differently\n" | |
| f"The strongest version of each is already selected (⭐). Change any you like, then press **Build my document**. " | |
| f"{same} exact copies will be merged automatically.") | |
| if not view: | |
| msg = (f"### No differently-worded duplicates found\n{same} exact copies will be merged automatically. " | |
| "Press **Build my document**.") | |
| if problems: | |
| msg += '\n\n**Skipped:** ' + '; '.join(problems) | |
| return msg, view, json.dumps(picks), gr.update(visible=True) | |
| def accept_all_best(groups): | |
| return json.dumps({str(g['id']): g['best'] for g in (groups or [])}) | |
| def _set_pick(cur, gid, val): | |
| try: | |
| d = json.loads(cur or '{}') | |
| except Exception: # noqa | |
| d = {} | |
| d[str(gid)] = int(val) | |
| return json.dumps(d) | |
| BLUE_CSS = """ | |
| .big-btn button, button.big-btn {min-height: 76px !important; font-size: 1.25rem !important; font-weight: 700 !important;} | |
| .ver {border: 1px solid #c7d7f2; border-radius: 10px; padding: 8px 12px; background: #f6f9ff;} | |
| .ver.best {border: 2px solid #1f5fbf; background: #eaf2ff;} | |
| .ver .refs {color: #1f4e99; font-size: .85rem;} | |
| """ | |
| with gr.Blocks(title='The Seamstresses') as demo: | |
| groups_state = gr.State([]) | |
| with gr.Tab('Merge documents'): | |
| gr.Markdown('# 🧵 The Seamstresses\n' | |
| '**Upload → Run → Review → Download.** Drop in every version of your documents - any type. ' | |
| 'You get back **one case summary** (parties, chronology, contradictions, laws cited, harm, requests) ' | |
| 'and **one master document** with the repeats taken out. Every line is quoted word for word with ' | |
| 'its source (document, page or time, paragraph). Nothing is invented or reworded.') | |
| files_new = gr.File(label='1. Your documents - PDF (also scanned), Word, text, email (.eml), spreadsheets, photos, audio or video', | |
| file_count='multiple', type='filepath') | |
| with gr.Accordion('Optional: Google Docs / Drive links, pasted text, a title', open=False): | |
| links_in = gr.Textbox(value='', label='Google Docs / Sheets / Drive links (shared as "Anyone with the link"), one per line', lines=2) | |
| paste_new = gr.Textbox(value='', label='Pasted text. Separate several versions with a line containing only ===', lines=5, max_lines=40) | |
| title_new = gr.Textbox(value='', label='Title (optional - otherwise taken from the documents)') | |
| gr.Markdown('### 2. How should duplicates be handled?') | |
| with gr.Row(equal_height=True): | |
| auto_btn = gr.Button('⚡ Choose for me', variant='primary', size='lg', elem_classes='big-btn') | |
| pick_btn = gr.Button('✋ Let me choose', variant='secondary', size='lg', elem_classes='big-btn') | |
| gr.Markdown('<small>⚡ picks the strongest version of every duplicate and builds straight away. ' | |
| '✋ shows the duplicates side by side with the strongest already selected, so you can change any.</small>') | |
| status_md = gr.Markdown() | |
| picks_box = gr.Textbox(value='{}', visible=False, label='picks_json') | |
| with gr.Column(visible=False) as review_col: | |
| groups_json = gr.JSON(visible=False) | |
| def _review(groups): | |
| for g in groups or []: | |
| with gr.Group(): | |
| gr.Markdown(f"#### Group {g['id']}" + (' ⚠️ these versions differ on dates, figures or yes/no - check carefully' if g['differ'] else '')) | |
| with gr.Row(equal_height=False): | |
| for vi, v in enumerate(g['versions']): | |
| with gr.Column(min_width=240, elem_classes='ver best' if vi == g['best'] else 'ver'): | |
| gr.Markdown(f"**Version {vi + 1}**{' ⭐ strongest' if vi == g['best'] else ''} \n" | |
| f"<span class='refs'>{v['refs']} · {v['files']} · {v['words']} words</span>") | |
| gr.Markdown('> ' + v['text'].replace('\n', ' ')) | |
| r = gr.Radio(choices=[(f"Keep version {i + 1}" + (' ⭐' if i == g['best'] else ''), i) for i in range(len(g['versions']))], | |
| value=g['best'], label=f"Group {g['id']} - which version goes in the document?", interactive=True) | |
| r.change(lambda val, cur, gid=g['id']: _set_pick(cur, gid, val), [r, picks_box], [picks_box], api_visibility='private') | |
| with gr.Row(): | |
| best_btn = gr.Button('✅ Accept all best', variant='secondary', size='lg') | |
| build_btn = gr.Button('🧵 Build my document', variant='primary', size='lg', elem_classes='big-btn') | |
| gr.Markdown('### 3. Download') | |
| with gr.Row(): | |
| docx_new = gr.File(label='Word (.docx)', interactive=False) | |
| pdf_new = gr.File(label='PDF', interactive=False) | |
| txt_new = gr.File(label='Text (.txt)', interactive=False) | |
| json_new = gr.File(label='Audit (.json)', interactive=False) | |
| preview_md = gr.Markdown() | |
| master_out = gr.Textbox(label='Master document (merged, wording exactly as uploaded)', lines=20, max_lines=2000) | |
| outs = [status_md, preview_md, master_out, docx_new, pdf_new, txt_new, json_new] | |
| auto_btn.click(lambda: gr.update(visible=False), None, [review_col], api_visibility='private').then( | |
| choose_for_me, [files_new, links_in, paste_new, title_new], outs, api_name='choose_for_me') | |
| pick_btn.click(let_me_choose, [files_new, links_in, paste_new], [status_md, groups_json, picks_box, review_col], | |
| api_name='let_me_choose').then(lambda g: g, [groups_json], [groups_state], api_visibility='private') | |
| best_btn.click(accept_all_best, [groups_state], [picks_box], api_visibility='private').then( | |
| build_with_picks, [files_new, links_in, paste_new, title_new, picks_box], outs, api_name='accept_all_best', api_visibility='private') | |
| build_btn.click(build_with_picks, [files_new, links_in, paste_new, title_new, picks_box], outs, api_name='build_with_picks') | |
| # legacy endpoint kept byte-for-byte for existing scripts (/stitch); not shown on screen | |
| with gr.Column(visible=False): | |
| files_in = gr.File(file_count='multiple', type='filepath') | |
| title_in = gr.Textbox(value='') | |
| paste_in = gr.Textbox(value='') | |
| btn = gr.Button('legacy stitch') | |
| docx_out = gr.File(); pdf_out = gr.File(); txt_out = gr.File() | |
| note_out = gr.Markdown() | |
| doc_out = gr.Textbox() | |
| btn.click(stitch_documents, inputs=[files_in, title_in, paste_in], | |
| outputs=[doc_out, docx_out, pdf_out, txt_out, note_out], api_name='stitch') | |
| with gr.Tab('Particulars of Claim'): | |
| gr.Markdown('## Particulars of Claim\n' | |
| 'Uses the documents you uploaded on the **Merge documents** tab - nothing to type. Repeats are merged, ' | |
| 'then the parties, dates, sums, references and the claims your documents support are picked out and ' | |
| 'drafted in court layout. Names from a court document (claim form, order) are used if there is one. ' | |
| 'Anything not found is left as **[CONFIRM: ...]**. Facts come only from your documents, law only from ' | |
| 'a fixed list of statutes, and no case law is cited.') | |
| c_btn = gr.Button('Create Particulars of Claim', variant='primary', size='lg', elem_classes='big-btn') | |
| c_status = gr.Markdown() | |
| with gr.Row(): | |
| c_docx = gr.File(label='Word (.docx)', interactive=False) | |
| c_pdf = gr.File(label='PDF', interactive=False) | |
| c_text = gr.Textbox(label='Preview', lines=24, max_lines=3000) | |
| c_btn.click(create_particulars, [files_new, links_in, paste_new, picks_box], [c_status, c_docx, c_pdf, c_text], | |
| api_name='create_particulars') | |
| with gr.Column(visible=False): # previous manual form, kept so /draft_poc, /draft_poc_json, /extract_facts still work | |
| gr.Markdown('## Particulars of Claim drafter (England & Wales)\n' | |
| 'Fill in what you know. Every fact pleaded comes **only** from what you type or extract. Anything ' | |
| 'essential that is missing is marked **[CONFIRM: ...]**. The law cited comes from a fixed list of ' | |
| 'statutory provisions, and no case law is ever cited. Output: Word, PDF and text in court layout, ' | |
| 'plus a separate drafting notes file (not for service). Lines with several parts use **|** between them.') | |
| with gr.Accordion('1. Court and parties', open=True): | |
| with gr.Row(): | |
| p_court = gr.Dropdown(['County Court', 'High Court'], value='County Court', label='Court') | |
| p_court_detail = gr.Textbox(label='Hearing centre (County Court) or list (High Court)', placeholder='e.g. Central London') | |
| p_claim_no = gr.Textbox(label='Claim number', placeholder='leave blank if not yet issued') | |
| with gr.Row(): | |
| p_cl_name = gr.Textbox(label='Claimant full name') | |
| p_cl_pron = gr.Dropdown(['he', 'she', 'they', 'it'], value='they', label='Claimant pronoun') | |
| p_cl_co = gr.Checkbox(label='Claimant is a company', value=False) | |
| p_cl_cap = gr.Textbox(label='Claimant status', placeholder='e.g. an individual who bought the goods for his own personal use') | |
| with gr.Row(): | |
| p_dob = gr.Textbox(label='Claimant date of birth (personal injury claims only)', placeholder='dd/mm/yyyy') | |
| p_addr = gr.Textbox(label='Address for service') | |
| p_signed = gr.Textbox(label='Signed by (leave blank if the Claimant is in person)') | |
| p_defs = gr.Textbox(label='Defendant(s), one per line: name | description | company no. | other names used in your facts', lines=2, | |
| placeholder='Acme Widgets Ltd | a company which sells widgets online | 01234567 | Acme | it\nJohn Smith | a director of the First Defendant | | Mr Smith | he') | |
| p_against = gr.Textbox(label='Who each claim is against (only needed if there is more than one Defendant), one per line: claim | defendant numbers | jointly and severally / several | capacities', | |
| lines=3, placeholder='deceit | 2, 1 | jointly and severally | 2 personally; 1 vicariously for 2\ns75 | 3\n(capacities: "N personally", "M vicariously for N", "N as agent of M")') | |
| p_terms = gr.Textbox(label='Defined terms, one per line: short name | full description | other words you use for it', lines=2, | |
| placeholder='Scooter | a Voltrider V9 Pro electric scooter, serial no. X | scooter, e-scooter') | |
| with gr.Accordion('2. The contract (if any)', open=False): | |
| with gr.Row(): | |
| p_goods = gr.Textbox(label='Short name of the goods/service (one of your defined terms)') | |
| p_c_how = gr.Textbox(label='How the contract was made', placeholder="e.g. an online order placed through the Defendant's website") | |
| p_c_ref = gr.Textbox(label='Order or contract reference') | |
| with gr.Row(): | |
| p_c_date = gr.Textbox(label='Contract date') | |
| p_c_obl = gr.Textbox(label='What the Defendant agreed to do (optional)', placeholder='e.g. provide the Account to the Claimant on the Terms') | |
| p_price = gr.Textbox(label='Price (£)') | |
| with gr.Row(): | |
| p_paid_d = gr.Textbox(label='Date paid') | |
| p_paid_h = gr.Textbox(label='How paid', placeholder='e.g. Visa debit card') | |
| p_deliv = gr.Textbox(label='Delivery date') | |
| p_express = gr.Textbox(label='Express terms, one per line: where found | what it says', lines=2) | |
| p_cdef = gr.Textbox(label='Which Defendant is the other party to the contract (number)', value='1') | |
| with gr.Accordion('3. The facts', open=True): | |
| p_facts = gr.Textbox(label='Facts, one per line: date | what happened | source document | tags (optional: injury, refusal, knowledge, notified, s75, omit, separate). Plain facts only - opinion, rhetoric and document references are stripped and listed in the notes', | |
| lines=10, max_lines=200, | |
| placeholder='2026-03-21 | the front brake failed and I was thrown onto the road | my witness statement | incident, injury') | |
| with gr.Row(): | |
| p_x_files = gr.File(label='Optional: extract dated sentences from documents (e.g. your merged document)', | |
| type='filepath', file_count='multiple') | |
| p_x_paste = gr.Textbox(label='...or from pasted text', lines=4) | |
| p_x_btn = gr.Button('Extract dated facts (word for word) into the facts box') | |
| p_x_note = gr.Markdown() | |
| with gr.Accordion('4. Causes of action and particulars', open=True): | |
| p_causes = gr.CheckboxGroup([(v, k) for k, v in poc.CAUSES.items()], label='Causes of action') | |
| gr.Markdown('Fill in only the boxes for the causes you ticked. Use one item per line.') | |
| with gr.Row(): | |
| p_breaches = gr.Textbox(label='Contract: breaches of express terms', lines=3) | |
| p_defects = gr.Textbox(label='Goods: defects', lines=3) | |
| p_failings = gr.Textbox(label='Negligence: failings', lines=3) | |
| with gr.Row(): | |
| p_desc = gr.Textbox(label='CRA s.11: description the goods were sold by') | |
| p_desc_mis = gr.Textbox(label='CRA s.11: how they did not match it', lines=2) | |
| p_purpose = gr.Textbox(label='CRA s.10: particular purpose') | |
| p_purpose_how = gr.Textbox(label='CRA s.10: how it was made known') | |
| with gr.Row(): | |
| p_defect_d = gr.Textbox(label='Date the defect became apparent') | |
| p_rej_d = gr.Textbox(label='Date goods rejected') | |
| p_rej_how = gr.Textbox(label='How rejected', placeholder='e.g. by email to the Defendant') | |
| p_cpa_role = gr.Dropdown(['', 'producer', 'own-brander', 'importer'], value='', label='CPA 1987: Defendant is the') | |
| with gr.Row(): | |
| p_svc_fail = gr.Textbox(label='CRA s.49: failures of care and skill', lines=2) | |
| p_duty = gr.Textbox(label='Negligence: scope of the duty (optional)') | |
| p_incident = gr.Textbox(label='Negligence: the incident', placeholder='e.g. accident on 21 March 2026') | |
| with gr.Row(): | |
| p_reps = gr.Textbox(label='Misrepresentation: date | how made, by whom | words used', lines=2) | |
| p_falsity = gr.Textbox(label='Why each was false', lines=2) | |
| p_knowledge = gr.Textbox(label='Deceit: facts showing knowledge or recklessness', lines=2) | |
| p_rescind = gr.Textbox(label='Rescission: what the Claimant is ready and willing to return', placeholder='e.g. the Bicycle') | |
| with gr.Row(): | |
| p_card = gr.Textbox(label='s.75: credit card agreement', placeholder='a Visa credit card agreement, account ending 1234') | |
| p_psr_tx = gr.Textbox(label='PSRs: transactions, one per line: date | amount | payee/description', lines=2) | |
| p_psr_how = gr.Textbox(label='PSRs (defective execution): what went wrong') | |
| with gr.Row(): | |
| p_bsd = gr.Textbox(label='Breach of statutory duty: provision | duty | how breached', lines=2) | |
| p_ue_amt = gr.Textbox(label='Unjust enrichment: sum received (£)') | |
| p_ue_how = gr.Textbox(label='How it was received', placeholder='being the deposit the Claimant paid on 1 May 2026') | |
| p_ue_factor = gr.Dropdown(['failure of basis', 'mistake'], value='failure of basis', label='Why it is unjust') | |
| p_ue_basis = gr.Textbox(label='The basis that failed / the mistake') | |
| with gr.Row(): | |
| p_imp = gr.Textbox(label='EqA: impairment') | |
| p_effects = gr.Textbox(label='EqA: effects on day-to-day activities', lines=2) | |
| p_service = gr.Textbox(label='EqA: the service provided') | |
| with gr.Row(): | |
| p_arising = gr.Textbox(label='EqA s.15: the "something arising"') | |
| p_unfav = gr.Textbox(label='EqA s.15: unfavourable treatment', lines=2) | |
| p_pcp = gr.Textbox(label='EqA s.19/20: provision, criterion or practice') | |
| p_disadv = gr.Textbox(label='EqA s.19/20: disadvantage') | |
| with gr.Row(): | |
| p_adj = gr.Textbox(label='EqA s.20: adjustments that should have been made', lines=2) | |
| p_harass = gr.Textbox(label='EqA s.26 / PHA: conduct relied on', lines=2) | |
| p_pdata = gr.Textbox(label='GDPR: personal data concerned') | |
| p_gdpr = gr.Textbox(label='GDPR: Article | what the Defendant did', lines=2, placeholder='5(1)(d) | recorded inaccurate arrears') | |
| with gr.Accordion('5. Loss, interest and remedies', open=True): | |
| p_losses = gr.Textbox(label='Losses, one per line: description | amount (or "general") | which claims caused it (optional, e.g. cra_goods, negligence)', lines=4) | |
| with gr.Row(): | |
| p_injuries = gr.Textbox(label='Injuries (personal injury), one per line', lines=2) | |
| p_medrep = gr.Textbox(label='Medical report relied on (expert and date)') | |
| with gr.Row(): | |
| p_aggr = gr.Textbox(label='Grounds for aggravated damages (optional)') | |
| p_injn = gr.Textbox(label='Injunction sought (optional)', placeholder='restraining the Defendant from ...') | |
| with gr.Row(): | |
| p_i_sum = gr.Textbox(label='Interest: specified sum (£)') | |
| p_i_rate = gr.Textbox(label='Rate % per year', placeholder='e.g. 8') | |
| p_i_from = gr.Textbox(label='From (date)') | |
| p_i_to = gr.Textbox(label='To (date)') | |
| p_value = gr.Textbox(label='Statement of value (optional)', placeholder='e.g. not more than £10,000') | |
| p_btn = gr.Button('Draft Particulars of Claim', variant='primary', size='lg') | |
| with gr.Row(): | |
| p_docx = gr.File(label='Word (.docx)', interactive=False) | |
| p_pdf = gr.File(label='PDF', interactive=False) | |
| p_txt = gr.File(label='Text', interactive=False) | |
| p_notes = gr.File(label='Drafting notes (not for service)', interactive=False) | |
| p_note = gr.Markdown() | |
| p_text = gr.Textbox(label='Draft Particulars of Claim', lines=30, max_lines=3000) | |
| with gr.Accordion('Advanced: draft from JSON (all fields as one object)', open=False): | |
| p_json = gr.Textbox(label='JSON', lines=6) | |
| p_json_btn = gr.Button('Draft from JSON') | |
| poc_inputs = [p_court, p_court_detail, p_claim_no, p_cl_name, p_cl_pron, p_cl_co, p_cl_cap, p_dob, p_addr, p_signed, | |
| p_defs, p_terms, p_goods, p_c_how, p_c_ref, p_c_date, p_c_obl, p_price, p_paid_d, p_paid_h, p_deliv, | |
| p_express, p_facts, p_causes, p_breaches, p_desc, p_desc_mis, p_purpose, p_purpose_how, p_defects, | |
| p_defect_d, p_rej_d, p_rej_how, p_failings, p_svc_fail, p_duty, p_incident, p_cpa_role, p_reps, | |
| p_falsity, p_knowledge, p_imp, p_effects, p_service, p_arising, p_unfav, p_pcp, p_disadv, p_adj, | |
| p_harass, p_pdata, p_gdpr, p_losses, p_injuries, p_medrep, p_aggr, p_injn, p_i_sum, p_i_rate, | |
| p_i_from, p_i_to, p_value, p_against, p_cdef, p_card, p_psr_tx, p_psr_how, p_bsd, p_ue_amt, p_ue_how, | |
| p_ue_factor, p_ue_basis, p_rescind] | |
| poc_outputs = [p_text, p_docx, p_pdf, p_txt, p_notes, p_note] | |
| p_btn.click(draft_poc, inputs=poc_inputs, outputs=poc_outputs, api_name='draft_poc') | |
| p_json_btn.click(draft_poc_json, inputs=[p_json], outputs=poc_outputs, api_name='draft_poc_json') | |
| p_x_btn.click(extract_poc_facts, inputs=[p_x_files, p_x_paste, p_facts], outputs=[p_facts, p_x_note], api_name='extract_facts') | |
| if __name__ == '__main__': | |
| demo.launch(theme=gr.themes.Soft(primary_hue='blue', secondary_hue='sky', neutral_hue='slate'), css=BLUE_CSS) | |