# inplace_chat.py import os import json import difflib import streamlit as st from huggingface_hub import InferenceClient # === Page header === st.header("In-place feedback chatbot demo") # === Theme primary color (from .streamlit/config.toml) === PRIMARY = "#cb785c" # keep in sync with [theme] primaryColor # HF Inference client hf_token = os.getenv("HF_TOKEN") client = InferenceClient(model="openai/gpt-oss-20b", token=hf_token) ####### initialize session states ###### # These are managed globally in streamlit_app.py, but we ensure they exist if "current_chat_id" not in st.session_state: st.session_state.current_chat_id = None if "conversations" not in st.session_state: st.session_state.conversations = {} # Get or create current conversation chat_id = st.session_state.current_chat_id if chat_id is None or chat_id not in st.session_state.conversations: # This shouldn't happen if streamlit_app.py is the entry point, but just in case import uuid chat_id = str(uuid.uuid4()) st.session_state.conversations[chat_id] = { "title": "New chat", "messages": [], "editable": False, "prev_text": "", "edited_text": "", "original_user_prompt": "", "edit_history": [] } st.session_state.current_chat_id = chat_id conv = st.session_state.conversations[chat_id] # Ensure all required keys exist in the current conversation conv.setdefault("messages", []) conv.setdefault("editable", False) conv.setdefault("prev_text", "") conv.setdefault("edited_text", "") conv.setdefault("original_user_prompt", "") conv.setdefault("title", "New chat") conv.setdefault("edit_history", []) # List of {removed, added, timestamp} # Shorthand references chat_history = conv["messages"] # === Utility: compact "removed vs added" summary for the inplace_prefix === ### later separate this into parse_diff.py def summarize_edit(old: str, new: str) -> tuple[str, str]: """ Produce coarse 'removed_text' and 'edited_text' by joining all deletions and insertions. Good enough for a single bullet like: - Replaced {removed_text} with {edited_text} """ old_words = old.split() new_words = new.split() sm = difflib.SequenceMatcher(a=old_words, b=new_words) removed_chunks, added_chunks = [], [] for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag in ("delete", "replace"): chunk = " ".join(old_words[i1:i2]).strip() if chunk: removed_chunks.append(chunk) if tag in ("insert", "replace"): chunk = " ".join(new_words[j1:j2]).strip() if chunk: added_chunks.append(chunk) removed_text = " / ".join(removed_chunks) if removed_chunks else "(none)" edited_text = " / ".join(added_chunks) if added_chunks else "(none)" return removed_text, edited_text def get_detailed_diff(old: str, new: str) -> list[dict]: """ Returns a list of diff chunks with tags: 'equal', 'delete', 'insert', 'replace' Each chunk has: {'tag': str, 'text': str} """ old_words = old.split() new_words = new.split() sm = difflib.SequenceMatcher(a=old_words, b=new_words) diff_chunks = [] for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == 'equal': diff_chunks.append({'tag': 'equal', 'text': ' '.join(old_words[i1:i2])}) elif tag == 'delete': diff_chunks.append({'tag': 'delete', 'text': ' '.join(old_words[i1:i2])}) elif tag == 'insert': diff_chunks.append({'tag': 'insert', 'text': ' '.join(new_words[j1:j2])}) elif tag == 'replace': diff_chunks.append({'tag': 'delete', 'text': ' '.join(old_words[i1:i2])}) diff_chunks.append({'tag': 'insert', 'text': ' '.join(new_words[j1:j2])}) return diff_chunks # === Render current conversation === editable = conv["editable"] for i, msg in enumerate(chat_history): if editable and i == len(chat_history) - 1 and msg["role"] == "assistant": continue with st.chat_message(msg["role"]): st.markdown(msg["content"]) if prompt := st.chat_input("Send a message"): conv["original_user_prompt"] = prompt chat_history.append({"role": "user", "content": prompt}) # Update conversation title with first user message if conv["title"] == "New chat" and len(chat_history) == 1: conv["title"] = prompt[:50] # Use first 50 chars of first message with st.chat_message("user"): st.markdown(prompt) messages_for_api = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": prompt}, ] with st.chat_message("assistant"): with st.spinner("Thinking…"): try: response = client.chat_completion( messages=messages_for_api, max_tokens=60000, temperature=0.7, top_p=0.95, stream=False, ) reply = response.choices[0].message["content"] st.markdown(reply) chat_history.append( {"role": "assistant", "content": reply} ) conv["prev_text"] = reply except Exception as e: st.error(f"Error: {e}") # Edit features if chat_history and chat_history[-1]["role"] == "assistant": # button with st.chat_message("assistant"): col_spacer, col_edit = st.columns([0.93, 0.07]) with col_edit: if not conv["editable"]: if st.button("✏️", key="edit_btn", help="Edit response"): conv["editable"] = True st.rerun() ######## Edit mode ######### if conv["editable"]: st.markdown('
', unsafe_allow_html=True) with st.chat_message("assistant"): st.caption("Editing the last response…") with st.form("edit_form", clear_on_submit=False): conv["edited_text"] = st.text_area( " ", value=conv["prev_text"], height=500, label_visibility="collapsed", key="edit_textarea", ) finished_edit = st.form_submit_button( "Finish edit", icon=":material/edit:", use_container_width=True ) st.markdown("
", unsafe_allow_html=True) # === Handle edit submission (backend: in-place continuation) === if conv["editable"] and finished_edit: if chat_history and chat_history[-1]["role"] == "assistant": chat_history.pop() removed_text, added_text = summarize_edit(conv["prev_text"], conv["edited_text"]) # Save to edit history import datetime diff_chunks = get_detailed_diff(conv["prev_text"], conv["edited_text"]) conv["edit_history"].append({ "removed": removed_text, "added": added_text, "timestamp": datetime.datetime.now().strftime("%H:%M:%S"), "original": conv["prev_text"], "edited": conv["edited_text"], "diff_chunks": diff_chunks }) system_prompt = "The user edited your previous answer. Only continue from the assistant message above. Do NOT rewrite or repeat it." # Exit edit mode before generation conv["editable"] = False with st.chat_message("assistant"): with st.spinner("Continuing from your edit…"): try: resp = client.chat_completion( messages=[ {"role": "system", "content": system_prompt}, {"role": "assistant", "content": conv["edited_text"]}, {"role": "user", "content": "Continue exactly from the assistant message above. Do not restate any of it; just append."}, ], max_tokens=60000, temperature=0.7, top_p=0.95, ) generated = resp.choices[0].message["content"] combined = conv["edited_text"] + '\n\n'+generated conv["prev_text"] = combined chat_history.append( {"role": "assistant", "content": combined} ) st.rerun() except Exception as e: st.error(f"Error while continuing from edit: {e}") chat_history.append( {"role": "assistant", "content": conv["prev_text"]} ) conv["editable"] = False