Skip to content
Draft
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
4 changes: 4 additions & 0 deletions src/clients/Client.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

#include <juce_core/juce_core.h>

#include "../widgets/StatusAreaWidget.h"

#include "../utils/Enums.h"
#include "../utils/Errors.h"
#include "../utils/Labels.h"
Expand Down Expand Up @@ -292,6 +294,8 @@ class Client
String getCommonHeaders() const { return getAuthorizationHeader() + acceptHeader; }
String getJSONHeaders() const { return getCommonHeaders() + contentTypeJSONHeader; }

SharedResourcePointer<StatusMessage> statusMessage;

private:
String getAuthorizationHeader() const
{
Expand Down
217 changes: 204 additions & 13 deletions src/clients/GradioClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ class GradioClient : public Client
{
Complete,
Heartbeat,
Error
//Generating
Info,
Warning,
Error,
Generating
};

GradioClient()
Expand Down Expand Up @@ -670,42 +672,231 @@ class GradioClient : public Client
return result;
}

// Collect any info/warning/error logs
StringArray infoLogs;
StringArray warningLogs;

// Stream the SSE response
while (! stream->isExhausted())
{
response = stream->readNextLine();

DBG_AND_LOG("GradioClient::makeGETRequest: Streamed response \"" << response << "\".");

if (response.containsIgnoreCase(enumToString(GradioEvents::Complete)))
if (response.isEmpty())
{
continue;
}
else if (response.containsIgnoreCase("event: " + enumToString(GradioEvents::Complete)))
{
response = extractPayLoad(stream->readNextLine());

DBG_AND_LOG("GradioClient::makeGETRequest: Final response \"" << response << "\".");

break;
return OpResult::ok();
}
else if (response.containsIgnoreCase("event: " + enumToString(GradioEvents::Info)))
{
response = stream->readNextLine();

DBG_AND_LOG("GradioClient::makeGETRequest: Info response \"" << response << "\".");

response = extractPayLoad(response);

DynamicObject::Ptr responseDict;

result = stringJSONToDict(response, responseDict);

if (result.failed())
{
return result;
}

String msg;

static const Identifier logKey { "log" };
static const Identifier msgKey { "message" };

if (responseDict->hasProperty(logKey))
{
msg = responseDict->getProperty(logKey);
}
else if (responseDict->hasProperty(msgKey))
{
msg = responseDict->getProperty(msgKey);
}
else
{
return OpResult::fail(
JsonError { JsonError::Type::MissingKey,
JSON::toString(var(responseDict.get()), true),
logKey.toString() + " || " + msgKey.toString() });
}

if (msg.isNotEmpty())
{
infoLogs.add(msg);

// onRemoteMessage(RemoteMessageType::Info, msg);
statusMessage->setMessage("[info] " + msg);
}
}
// TODO - combine with above conditional
else if (response.containsIgnoreCase("event: " + enumToString(GradioEvents::Warning)))
{
response = stream->readNextLine();

DBG_AND_LOG("GradioClient::makeGETRequest: Warning response \"" << response
<< "\".");

response = extractPayLoad(response);

DynamicObject::Ptr responseDict;

result = stringJSONToDict(response, responseDict);

if (result.failed())
{
return result;
}

String msg;

static const Identifier logKey { "log" };
static const Identifier msgKey { "message" };

if (responseDict->hasProperty(logKey))
{
msg = responseDict->getProperty(logKey);
}
else if (responseDict->hasProperty(msgKey))
{
msg = responseDict->getProperty(msgKey);
}
else
{
return OpResult::fail(
JsonError { JsonError::Type::MissingKey,
JSON::toString(var(responseDict.get()), true),
logKey.toString() + " || " + msgKey.toString() });
}

if (msg.isNotEmpty())
{
warningLogs.add(msg);

// onRemoteMessage(RemoteMessageType::Warning, msg);
statusMessage->setMessage("[warning] " + msg);
}
}
else if (response.containsIgnoreCase(enumToString(GradioEvents::Error)))
else if (response.containsIgnoreCase("event: " + enumToString(GradioEvents::Error)))
{
response = stream->readNextLine();

DBG_AND_LOG("GradioClient::makeGETRequest: Error response \"" << response << "\".");

// TODO - could potentially identify other errors (e.g., too many requests)
/*
response = extractPayLoad(response);

// Some HF / ZeroGPU paths send "data: null"
bool isNullData =
errorPayload.isEmpty() || errorPayload == "null" || errorPayload == "None";

// Read a few more lines for additional context (stack traces, etc.)
juce::String additionalInfo;
const int maxContextLines = 10;
for (int i = 0; i < maxContextLines && ! stream->isExhausted(); ++i)
{
auto nextLine = stream->readNextLine();
if (nextLine.isEmpty())
continue;
if (nextLine.startsWith("event:"))
break; // next SSE event, stop
additionalInfo += nextLine + "\n";
}
additionalInfo = additionalInfo.trim();

// Default to remote application error, not HTTP error
error.type = ErrorType::RemoteAppError;

// interpret errorPayload as JSON
if (! isNullData)
{
var parsed = JSON::parse(errorPayload);
if (parsed.isObject())
{
if (auto* obj = parsed.getDynamicObject())
{
juce::String msg;
if (obj->hasProperty("error"))
msg = obj->getProperty("error").toString();
else if (obj->hasProperty("message"))
msg = obj->getProperty("message").toString();
else if (obj->hasProperty("detail"))
msg = obj->getProperty("detail").toString();

if (msg.isNotEmpty())
{
error.devMessage = msg;
onRemoteMessage(RemoteMessageType::Error, msg);
}
}
}

// If JSON parse failed or we didn't get a message, fall back to raw
if (error.devMessage.isEmpty())
error.devMessage = errorPayload;
}
else
{
// data:null case – we no longer assume "ZeroGPU quota".
// Treat it as a generic remote worker error, but keep any context we have.
if (additionalInfo.isNotEmpty())
error.devMessage = additionalInfo;
else
error.devMessage =
"The remote worker reported an error with no additional details.\n"
"Open the model's Hugging Face Space to inspect the exact cause.";
}

// Append any info/warning logs for more context
if (! infoLogs.isEmpty() || ! warningLogs.isEmpty())
{
juce::String logContext = "\n\n Logs from remote model \n";
for (auto& msg : infoLogs)
logContext += "[info] " + msg + "\n";
for (auto& msg : warningLogs)
logContext += "[warning] " + msg + "\n";
error.devMessage += logContext;
}
*/

return OpResult::fail(GradioError { GradioError::Type::RuntimeError, errorPath });
}
else if (response.contains("event: " + enumToString(GradioEvents::Heartbeat)))
{
// HEARTBEAT: keep-alive, ignore
continue;
}
else if (response.contains("event: " + enumToString(GradioEvents::Generating)))
{
// GENERATING / progress – currently we only debug-print
response = stream->readNextLine();

DBG_AND_LOG("GradioClient::makeGETRequest: Generating response \"" << response << "\".");

continue;
}
else
{
// TODO - what other information is available?
// Informational or progress events
// Examples:
// - event: heartbeat
// - event: log
// - event: progress
// TODO - handle this case
}
}

return OpResult::ok();
// If we exit the loop without a complete or error event, treat as an HTTP-ish error
error.code = statusCode;
error.devMessage = "Did not receive a complete or error event from the remote worker.";
return OpResult::fail(error);
}

OpResult downloadFile(String downloadPath, File& fileToDownload) //override
Expand Down
20 changes: 20 additions & 0 deletions src/utils/Errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,26 @@ inline String toUserMessage(const GradioError& e)
{
case GradioError::Type::RuntimeError:

/*
// Make the message a bit more friendly depending on common Python errors
if (error.devMessage.containsIgnoreCase("NameError"))
{
error.userMessage =
"The remote model crashed with a NameError (a variable or function wasn't defined).\n\n"
+ error.devMessage;
}
else if (error.devMessage.containsIgnoreCase("TypeError"))
{
error.userMessage =
"The remote model crashed with a TypeError (unexpected type passed in the model code).\n\n"
+ error.devMessage;
}
else
{
error.userMessage = "A remote processing error occurred in the model.\n\n"
+ error.devMessage;
}*/

userMessage = "A runtime error occurred at endpoint";

if (e.endpointPath.isNotEmpty())
Expand Down
66 changes: 62 additions & 4 deletions src/widgets/StatusAreaWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ class MessageBox : public Component, ChangeListener
public:
MessageBox(float fontSize = 15.0f, Justification justification = Justification::centred)
{
messageLabel.setJustificationType(juce::Justification::topLeft);
messageLabel.setFont(fontSize);
messageLabel.setColour(Label::textColourId, Colour(0xE0, 0xE0, 0xE0));
messageLabel.setColour(juce::Label::textColourId, juce::Colour(0xE0, 0xE0, 0xE0));
messageLabel.setInterceptsMouseClicks(false, false);
messageLabel.setMinimumHorizontalScale(1.0f);

messageLabel.setJustificationType(justification);
addAndMakeVisible(messageLabel);
viewport.setViewedComponent(&messageLabel, false);
viewport.setScrollBarsShown(true, false); // vertical yes, horizontal no
addAndMakeVisible(viewport);

sharedMessage->addChangeListener(this);
}
Expand All @@ -60,7 +64,13 @@ class MessageBox : public Component, ChangeListener
g.drawRect(getLocalBounds(), 1);
}

void resized() { messageLabel.setBounds(getLocalBounds()); }
void resized()
{
viewport.setBounds(getLocalBounds());

messageLabel.setSize(viewport.getWidth() - viewport.getScrollBarThickness(),
jmax(messageLabel.getHeight(), viewport.getHeight()));
}

void changeListenerCallback(ChangeBroadcaster* /*source*/)
{
Expand All @@ -69,9 +79,57 @@ class MessageBox : public Component, ChangeListener

private:
SharedResourcePointer<MessageType> sharedMessage;
Viewport viewport;
Label messageLabel;
};

/*
void StatusBox::appendRemoteMessage(const juce::String& level,
const juce::String& message)
{
juce::String prefix;

if (level == "info") prefix = "[info] ";
else if (level == "warning") prefix = "[warning] ";
else if (level == "error") prefix = "[error] ";

accumulatedMessages << prefix << message << "\n";

contentLabel.setText(accumulatedMessages, juce::dontSendNotification);

// Resize height to fit content
contentLabel.setSize(
viewport.getWidth() - viewport.getScrollBarThickness(),
contentLabel.getTextHeight()
);

// Auto scroll to bottom
viewport.setViewPosition(
0,
juce::jmax(0, contentLabel.getBottom() - viewport.getMaximumVisibleHeight())
);
repaint();
}

void StatusBox::setStatusMessage(const juce::String& message)
{
statusLabel.setText(message, juce::dontSendNotification);
accumulatedMessages = message;
contentLabel.setText(message, juce::dontSendNotification);

contentLabel.setSize(
viewport.getWidth() - viewport.getScrollBarThickness(),
contentLabel.getTextHeight()
);
}

void StatusBox::clearStatusMessage()
{
accumulatedMessages.clear();
contentLabel.setText({}, juce::dontSendNotification);
}
*/

using StatusBox = MessageBox<StatusMessage>;
using InstructionsBox = MessageBox<InstructionsMessage>;

Expand Down
Loading