diff --git a/ui/app.py b/ui/app.py index f5b005c..e8448bb 100644 --- a/ui/app.py +++ b/ui/app.py @@ -11,6 +11,9 @@ with open('styles.css', 'r') as file: css = file.read() +with open('script.js', 'r') as file: + js_code = f'' + def check_for_placeholders(data, placeholder): data = json.loads(data) if isinstance(data, str) else data if isinstance(data, dict): @@ -66,9 +69,7 @@ def validate_input(api_url, api_key, payload_format, request_body_kv, request_bo continue key, value = line.split(":") response_body[key.strip()] = value.strip() - - return generate_output(api_url, api_key, request_body, response_body, openai_key, model) def update_payload_format(payload_format): @@ -77,28 +78,113 @@ def update_payload_format(payload_format): else: return gr.update(visible=True), gr.update(visible=False) , gr.update(visible=True), gr.update(visible=False) -with gr.Blocks(css=css) as iface: +with gr.Blocks(css=css, head=js_code) as iface: gr.Markdown("# Whistleblower 📣\nA tool for leaking system prompts of LLM Apps, built by Repello AI.") + + # Main horizontal layout: left for inputs, right for output with gr.Row(): - with gr.Column(): - api_url = gr.Textbox(label='API URL', lines=1) - api_key = gr.Textbox(label='Optional API Key', lines=1) - payload_format = gr.Dropdown(choices=["Key-Value", "JSON"], label="Payload Format", value="Key-Value") - request_body_kv = gr.Textbox(label='Request body (replace input field value with $INPUT)', lines=3, placeholder='prompt: $INPUT') - request_body_json = gr.Textbox(label='Request body (replace input field value with $INPUT)', lines=3, placeholder='{\n\t"prompt": "$INPUT"\n}', visible=False) - response_body_kv = gr.Textbox(label='Response body (replace output field value with $OUTPUT)', lines=3, placeholder='response: $OUTPUT') - response_body_json = gr.Textbox(label='Response body (replace output field value with $OUTPUT)', lines=3, placeholder='{\n\t"response" : "$OUTPUT"\n}' , visible=False) - openai_key = gr.Textbox(label="OpenAI API Key") - model = gr.Dropdown(choices=["gpt-4o", "gpt-3.5-turbo", "gpt-4"], label="Model") - with gr.Column(): - output = gr.Textbox(label="Output", lines=27) - + # --- Left Column (Inputs) --- + # assign an elem_id so JS can reliably target this column + with gr.Column(elem_id="input-column"): + # Group for API connection inputs + with gr.Group(): + with gr.Row(): + # Textbox for the target API URL + api_url = gr.Textbox( + label="Target API URL", + lines=1, + placeholder="https://example.com/query", + info="Endpoint to send the synthesized request to" + ) + # Optional API key/token for the target API + api_key = gr.Textbox( + label="Optional API Key", + lines=1, + placeholder="Bearer ... or API key (if required)", + info="Optional key or token to access the target API" + ) + + # Group for payload format selection and body templates + with gr.Group(): + # Dropdown to choose Key-Value or JSON payload entry + payload_format = gr.Dropdown( + choices=["Key-Value", "JSON"], + label="Payload Format", + value="Key-Value", + info="Choose how you'll enter the request/response bodies" + ) + + # Two columns inside this group: request templates and response templates + with gr.Row(): + with gr.Column(): + # Key/Value style request body textbox (visible by default) + request_body_kv = gr.Textbox( + label='Request body — Key/Value (use $INPUT)', + lines=4, + placeholder='prompt: $INPUT\nuser_id: 123', + info="Enter each pair on its own line as key: value. Use $INPUT placeholder where the user input should go." + ) + # JSON style request body textbox (hidden by default) + request_body_json = gr.Textbox( + label='Request body — JSON (use $INPUT)', + lines=6, + placeholder='{\n \"prompt\": \"$INPUT\"\n}', + visible=False, + info="Enter a valid JSON payload. Include $INPUT somewhere in the values." + ) + + with gr.Column(): + # Key/Value style response body textbox (visible by default) + response_body_kv = gr.Textbox( + label='Response body — Key/Value (use $OUTPUT)', + lines=4, + placeholder='response: $OUTPUT\nconfidence: 0.9', + info="Each pair on its own line. Use $OUTPUT to indicate where the model output will be placed." + ) + # JSON style response body textbox (hidden by default) + response_body_json = gr.Textbox( + label='Response body — JSON (use $OUTPUT)', + lines=6, + placeholder='{\n \"response\": \"$OUTPUT\"\n}', + visible=False, + info="Enter a valid JSON template. Include $OUTPUT somewhere in the values." + ) + + # Group for OpenAI extraction key and model selection + with gr.Group(): + # Hidden/password textbox for the OpenAI API key + openai_key = gr.Textbox( + label="OpenAI API Key", + type="password", + placeholder="sk-...", + info="OpenAI key used to run the extraction model (kept hidden)." + ) + # Dropdown to select which OpenAI model to use + model = gr.Dropdown( + choices=["gpt-4o", "gpt-4", "gpt-3.5-turbo"], + label="Model", + value="gpt-4o", + info="Select the OpenAI model to use for extracting the hidden prompt." + ) + + # --- Right Column (Output) --- + # Single column that holds the non-interactive output textbox + # give it an elem_id so JS can target it and find the inner textarea reliably + with gr.Column(elem_classes="full-height-col"): + output = gr.Textbox( + label="Extracted Prompt", + lines=27, + interactive=False, + placeholder="The extracted system prompt will appear here after submission.", + elem_id="extracted-prompt" + ) + payload_format.change( fn=update_payload_format, inputs=payload_format, outputs=[request_body_kv, request_body_json , response_body_kv , response_body_json] ) - + submit_btn = gr.Button("Submit") submit_btn.click( fn=validate_input, diff --git a/ui/script.js b/ui/script.js new file mode 100644 index 0000000..a54fd9c --- /dev/null +++ b/ui/script.js @@ -0,0 +1,84 @@ +(function waitForElements(ids, cb, timeout = 10000, interval = 100) { + const start = Date.now(); + const iv = setInterval(() => { + const found = ids.map(id => document.querySelector(id)); + if (found.every(el => el !== null)) { + clearInterval(iv); + cb(...found); + return; + } + if (Date.now() - start > timeout) { + clearInterval(iv); + console.warn('waitForElements: timeout waiting for', ids); + } + }, interval); +})( + ['#input-column', '#extracted-prompt'], // selectors to wait for + (inputCol, outputWrapper) => { + // `outputWrapper` is the container that wraps the textbox; find the textarea inside it + const textarea = outputWrapper.querySelector('textarea') || document.querySelector('#extracted-prompt textarea'); + + if (!textarea) { + console.warn('extracted-prompt textarea not found inside wrapper, trying global query'); + } + + // helper to compute rows based on viewport or target height + function rowsForViewport(width, height) { + if(width < 400){ + return 3; + } + else if(height < 600){ + return Math.floor(height/20); + } + else if(height < 800){ + return Math.floor(height/25); + } + else{ + return Math.floor(height/30); + } + } + + function syncHeightAndRows() { + // attempt to get the current elements (in case of DOM reattach) + const src = document.querySelector('#input-column'); + const outWrapper = document.querySelector('#extracted-prompt'); + const ta = outWrapper ? outWrapper.querySelector('textarea') : textarea; + if (!src || !outWrapper || !ta) return; + + // match wrapper height + const targetHeight = src.offsetHeight; + if(window.innerWidth>500){ + outWrapper.style.height = targetHeight + 'px'; + } + else{ + outWrapper.style.height = 100 + 'px'; + } + // set textarea rows to fit target height (uses computed line-height) + const rows = rowsForViewport(window.innerWidth, window.innerHeight, targetHeight); + ta.rows = rows; + // also explicitly set textarea height (small buffer to avoid scroll) + const lineHeight = parseFloat(getComputedStyle(ta).lineHeight) || 20; + ta.style.height = (rows * lineHeight + 8) + 'px'; + } + + // initial sync + syncHeightAndRows(); + + // Observe input column for size changes and update target + try { + const ro = new ResizeObserver(syncHeightAndRows); + ro.observe(inputCol); + } catch (e) { + // fallback: listen to window resize + window.addEventListener('resize', syncHeightAndRows); + } + + // Also observe mutations inside the input column (in case content changes) + try { + const mo = new MutationObserver(syncHeightAndRows); + mo.observe(inputCol, { childList: true, subtree: true, characterData: true }); + } catch (e) { + // ignore if MutationObserver not available + } + } +); diff --git a/ui/styles.css b/ui/styles.css index 765c0fa..627c5a3 100644 --- a/ui/styles.css +++ b/ui/styles.css @@ -2,4 +2,12 @@ margin-left: auto; margin-right: auto; text-align: center; -} \ No newline at end of file +} + +#component-3, +#component-25 { + flex: 1 1 0; /* Equal flex-basis, equal growth */ + min-width: 0; /* Prevent overflow issues */ + width: 50%; /* Explicit 50% width */ + max-width: 50%; /* Prevent growing beyond 50% */ +}