"""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('⚡ 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.') 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) @gr.render(inputs=[groups_state]) 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"{v['refs']} · {v['files']} · {v['words']} words") 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)