Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 103 additions & 17 deletions ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
with open('styles.css', 'r') as file:
css = file.read()

with open('script.js', 'r') as file:
js_code = f'<script>{file.read()}</script>'

def check_for_placeholders(data, placeholder):
data = json.loads(data) if isinstance(data, str) else data
if isinstance(data, dict):
Expand Down Expand Up @@ -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):
Expand All @@ -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,
Expand Down
84 changes: 84 additions & 0 deletions ui/script.js
Original file line number Diff line number Diff line change
@@ -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
}
}
);
10 changes: 9 additions & 1 deletion ui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@
margin-left: auto;
margin-right: auto;
text-align: center;
}
}

#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% */
}