ColdFusion in Context: Human Help

Suppose you want to provide human help via interactive text to those who request it. ColdFusion can help you do that. Let's state the requirements, then code to meet them.

State Major Requirements

  1. Provide links from an existing application for users to receive human help. Such links should be the only change needed to the application. This should also be true of the administrative application and its links to provide human help.
  2. When the user clicks on the link, let human help read the user's cookie and do a database lookup to get some information about the user. For this demonstration, you'll add a vendor ID to the URL, and the "application" will set the cookie to match that URL. This is the only deviation between a real application and the one used for this demonstration. (The real one would set the cookie based on login.) The statement automatically sent to adminstrators when help is requested is "paging...".
  3. Visually alert all administrators who have their help window open that there is a request for help. When any statement other than "paging..." is the most recent statement, remove the visual alert.
  4. Let the administrator determine whom to send text to, make a statement, and send that statement. Selection is performed via a select box identifying each individual who is requesting or providing help. Sending a statement then involves typing in text and pressing {Enter} or clicking on the Send button.
  5. Let all administrators see all statements. Let users see only those statements sent to or from them. Administrators can converse with each other - all adminstrators see these statements - but vendors don't. Any administrator can take over the conversation with a vendor seamlessly with no indication to the vendor that this has occurred.
  6. Administrators can terminate a vendor's help session. A vendor can close its own help session. Either way, the vendor is removed from the list of vendors seeking help, and the vendor's help window closes.

Provide Initial Data

Make some tables.
  1. tblAdmin contains AdminID and Name; it simulates the admin application. Supply a couple of rows. The exercises at the end of this tip use IDs 4 and 5.
  2. tblVendor contains VendorID (a number), FirstName, and CompanyName; it simulates the vendor side of the application. Supply a couple of rows. The exercises at the end of this tip use IDs 118 and 120.
  3. tblTalker contains Userid (a number), TalkLtr, SentDt (a date/time), Company, and Name; it will tell who's asking for or providing help. Leave it empty.
  4. tblTalk contains StatementKey (automatically incremented by the database), FromLtr, FromID (a number), FromCompany, FromName, ToLtr, ToID (a number), SentDt (a date/time), Statement, and Retired (a number); it will contain all conversations. Leave it empty.

"Fake" Applications

You'll need a stand-in for the vendor's application and a stand-in for the admin portion of the application. Typically, a cookie would be used to identify the user after login. To avoid creating a full-blown application to demonstrate human help, make an admin page that sets the cookie to a value you supply in the URL as if it had been set by the application; call it adminapp.cfm. Set TalkRole in the link to provide double confirmation that this individual will provide help rather than receive it. (Human help can't rely on the cookie alone to know which role this window should play; because, because both a user cookie and an admin cookie may be present during demonstrations where a single workstation plays multiple roles.) Look carefully at the link; setting the target to the special value "_blank" opens a new window.

<!--- For demonstration, set ID cookie for Admin
as if it came from an external application
and give this window an Admin role in the url --->
<cfparam name="url.id" default="">
<cfif not len(trim(url.id))>
  This page acts like a page in your Admin application.
  For demonstration, you must follow the URL with
  ?ID={external Admin userid}
  <cfabort>
</cfif>
<cfcookie name="curruser" value="#url.id#">
I'm ready to provide ...
<a href="tu.cfm?&TalkRole=1" target="_blank">Human Help</a>

Here's a page that does the same thing for users, except that TalkRole is not used; call it userapp.cfm. In both cases, all that's happening here is that a user or administrator can click on a link that opens a new window to tu.cfm. The application sets a user or admin environment that will be sensed by human help.

<!--- For demonstration, set ID cookie for user
as if it came from an external application --->
<cfparam name="url.id" default="">
<cfif not len(trim(url.id))>
  This page acts like a page in your user application.
  For demonstration, you must follow the URL with
  ?ID={external user's userid}
  <cfabort>
</cfif>
<cfcookie name="currentvendor" value="#url.id#">
I'm ready to receive ...
<a href="tu.cfm" target="_blank">Human Help</a>

Identify Users

Put this code in tuid.cfm to identify the user and the user's role. (tu.cfm will include this file as one of its first acts.) If you can't determine the role and ID, complain and stop.

If a cookie for an administrator is present and TalkRole is defined and not left empty in the URL, then set an ID for use in the human help application to the value of the cookie, set the variable AdminURL to "&url.TalkRole=1", and set a value in pixels that will be used to control the height of the control frame in a human help frameset. The unusual value for AdminURL helps it be concatenated to a query string later.

If this combination of conditions is not true but a vendor cookie is present, do the almost the same thing. Differences: make AdminRole empty in the URL, and set a smaller control height than you did for an administrator.

<!--- Set "my" ID and parameters
from calling URL and external application's cookie --->
<cfset MyID=0>
<cfif isDefined("url.TalkRole")
and len(trim(url.TalkRole))
and isDefined("cookie.curruser")>
  <cfset MyID=val(cookie.curruser)>
  <cfset AdminURL="&TalkRole=1">
  <cfset ControlHeight=170>
<cfelseif isDefined("cookie.currentvendor")>
  <cfset MyID=val(cookie.currentvendor)>
  <cfset AdminURL="">
  <cfset ControlHeight=120>
</cfif>
<cfif not MyID>
  Can't identify you; can't continue.
  <cfabort>
</cfif>

This approach to working with what amounts to two external applications assumes that the ID for an administrator won't be the same as the ID for a vendor. If you can't guarantee that IDs won't overlap in the applications, you can keep them from overlapping in human help by modifying the code to add a arbitrary value to the vendor ID (or the admin ID) and by modifying the code that uses IDs to check external tables.

Prepare Frameset or Stop

Put this code in tu.cfm to handle human help through a frameset. The frameset will usually display a control frame, a conversation frame, and a driver. First, however, it has work to do: identify the user and role, stop if Mode in the URL says "bye", get or start a help session, and open the frameset.

Include tuid.cfm to get the ID, role, and (as previously mentioned), the desired height of the control frame. However, if the URL mode says "bye", include tutalk.cfm to respond "bye", include tudrop.cfm to drop the help session - more about this later - and stop without opening the frameset. Therefore, using this URL to call tuid.cfm shuts down the help session.

<!--- Get my ID, Role, and parameters --->
<cfinclude template="tuid.cfm">
<!--- If URL mode says "bye", stop --->
<cfparam name="url.Mode" default="">
<cfif url.Mode is "bye">
  <cfset Statement="bye">
  <cfinclude template="tutalk.cfm">
  <cfset DropID=MyID>
  <cfinclude template="tudrop.cfm">
  This session has ended; please close this window.
  <cfabort>
</cfif>

To make it easy for administrators to specify which user they're directing their replies to, each help session is identified by a letter of the alphabet. Therefore, if the URL doesn't say "bye", the next step is to determine that letter. If the letter isn't known, include tustart.cfm to assign a new one. If the letter is known but isn't in tblTalker, include tustart.cfm to assign a new one. At the end of this process, url.MyLtr will contain that letter.

<!--- Get or start help session;
confirm or provide my letter --->
<cfparam name="url.MyLtr" default="">
<cfif not len(trim(url.MyLtr))>
  <cfinclude template="tustart.cfm">
<cfelse>
  <cfquery name="talkerGet" datasource="context">
  select * from tblTalker
  where Userid = #val(MyID)#
  </cfquery>
  <cfif not talkerGet.recordcount>
    <cfinclude template="tustart.cfm">
  </cfif>
</cfif>

Now it's time to display the frames. Set their query strings so that MyLtr is the appropriate letter and so that AdminURL is "&TalkRole=1" for admin or empty for other users. Set the height of the control frame as specified by tuid.cfm earlier. To keep the driver frame from being seen, set its height to 1 pixel, and prevent it from scrolling and from being resized. (The driver frame refreshes periodically. If it can't be seen, the refresh won't bother the user.)

<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">
<html>
<head>
<title>Human Help</title>
<!--- Place frameset BETWEEN head and body --->
</head>
<cfoutput>
<frameset rows="#ControlHeight#,*,1"
frameborder="0" border="0">
<frame name="control" src="tucontrol.cfm?#Extra#">
<frame name="conversation" src="tushow.cfm?#Extra#">
<frame name="driver" src="tudrive.cfm?#Extra#"
noresize scrolling="no">
</frameset>
</cfoutput>
<body>
<noframes>
Please use a browser that understands frames.
</noframes>
</body>
</html>

Start a Help Session

Put this code in tustart.cfm to get a new letter assignment. (Remember that when a user first asks for help or offers help, MyLtr won't exist, and tu.cfm will call this file to get it.) The first step is to get external data about the users by including tuexdata.cfm. Then assign a letter to this help session.

The SQL syntax of the delete statement for a production database such as Oracle or MS-SQL is different from the syntax for Microsoft Access. This code uses the Microsoft Access syntax if it's being run in a test environment (127.0.0.1 or "futureec") instead of in a production environment. Because the letter identifying this user for human help wasn't in the URL, retire any statements consisting of "bye" going from or going to this ID in tblTalker. (These statements are left over from the previous session for reasons that will be discussed later.)

<!--- Get external data based on ID and role --->
<cfinclude template="tuexdata.cfm">
<!--- Set my "letter" --->
<cfquery name="talkerDrop" datasource="context">
<cfif cgi.server_name contains "127"
  or cgi.server_name contains "futureec.com">
  delete * from tblTalker
<cfelse>
  delete from tblTalker
</cfif>
where Userid = #val(MyID)#
</cfquery>
<cfquery name="talkDrop" datasource="context">
update tblTalk
set Retired = 1
where Statement = 'bye'
and (FromID = #val(MyID)#
or ToID = #val(MyID)#)
</cfquery>

If this is not an administrator, look for a letter from B through Z (chr 66 through 90) that is NOT used in a query of tblTalker. (The letter A is reserved for administrators; so, it's not considered here.) When an unassigned letter is found, break from the loop and assign it. If all letters from B to Z are used, then complain.

If this is an administrator, simply use the letter A (chr 65) without a search.

When done, set url.MyLtr to the letter you have selected.

<cfif not len(trim(AdminURL))>
  <cfquery name="talkerList" datasource="context">
  select TalkLtr from tblTalker
  </cfquery>
  <cfset LtrList="">
  <cfloop query="talkerList">
  <cfset LtrList=listAppend(LtrList,TalkLtr)>
  </cfloop>
  <cfset OK=0>
  <!--- (Loop from B to Z) --->
  <cfloop from="66" to="90" index="LtrCode">
  <cfif not listContains(LtrList, chr(LtrCode))>
    <cfset OK=1>
    <cfbreak>
  </cfif>
  </cfloop>
  <cfif not OK>
    Try again later; too many need help now.
    <cfabort>
  </cfif>
<cfelse>
  <cfset LtrCode=65>
</cfif>
<cfset url.MyLtr=chr(LtrCode)>

Now add this user to tblTalker. The Name and Company data come from the user side of the external application or the admin side of that application as appropriate. Finally, include tutalk.cfm to add the special statement "...paging" to tuTalk where an administrator can see it.

<!--- Add me to list of talkers --->
<cfquery name="talkerAdd" datasource="context">
insert into tblTalker
(TalkLtr, Userid,
SentDt, Company,
Name)
values
('#url.MyLtr#', #val(MyID)#,
#createODBCDateTime(now())#, '#Company#',
'#Name#')
</cfquery>

<!--- Add initial statement --->
<cfset form.Statement="paging...">
<cfinclude template="tutalk.cfm">

Get External Data

The previous description glossed over how to get data from external applications. Put this code in tuexdata.cfm to perform that function. If this is an administrator, get information from tblAdmin (part of the admin application); complain if you can't. If this isn't an administrator, get information from tblVendor; complain if you can't. Because the data isn't defined the same way in these two sources, adjust the data coming from tblVendor to be consistent in length and name with the data provided for administrators (for simplicity). Grab the first 40 characters of the CompanyName and call it Company. Grab the first 15 characters of the FirstName and call it Name. Sp5, which consists of 5 spaces, is handy to pad strings so they can be truncated to the desired length before they're trimmed.

<!--- Get external data based on ID and role --->
<cfif len(trim(AdminURL))>
  <cfquery name="adminGet" datasource="context">
  select * from tblAdmin
  where AdminID = #val(MyID)#
  </cfquery>
  <cfif not AdminGet.recordcount>
    Admin not in application.
    <cfabort>
  </cfif>
  <cfset Company="Admin">
  <cfset Name=adminGet.Name>
<cfelse>
  <cfquery name="userGet" datasource="context">
  select VendorID, CompanyName,
  FirstName from tblVendor
  where VendorID = #val(MyID)#
  </cfquery>
  <cfif not userGet.recordcount>
    User not in application.
    <cfabort>
  </cfif>
  <cfset sp5="     ">
  <cfset Company=trim(left(
  userGet.CompanyName&sp5&sp5&sp5&sp5,40))>
  <cfset Name=trim(left(
  userGet.FirstName&sp5&sp5&sp5,15))>
</cfif>

"Send" a Statement

Now you have enough information to talk to human help; put this code in tutalk.cfm. To transmit a statement, update tblTalker to show that it's still actively sending data, and add a row to tblTalk so it can be seen by adminstrators and by this user. If you don't know certain values - you might not know them when a help session is about to end - supply default values.

<!--- Set when this talker last sent something --->
<cfquery name="talkerUpd" datasource="context">
update tblTalker
set SentDt = #createODBCDate(now())#
where Userid = #val(MyID)#
</cfquery>

<!--- Add the statement --->
<cfparam name="ToLtr" default="A">
<cfparam name="ToID" default=0>
<cfparam name="Company" default="">
<cfparam name="Name" default="">
<cfquery name="sendStatement" datasource="context">
insert into tblTalk
(FromLtr, FromID,
FromCompany, FromName,
ToLtr, ToID,
SentDt, Statement,
Retired)
values
('#url.MyLtr#', #val(MyID)#,
'#Company#', '#Name#',
'#ToLtr#', '#ToID#',
#createODBCDateTime(now())#, '#Statement#',
0)
</cfquery>

If an adminstrator says "bye" to a user, and the user's ID is known, then include tudrop.cfm to drop the user. Once this is done, use javascript to reload the control frame so that the user won't be displayed in its list of active sessions. Because this is an administrator, its letter is "A" and its TalkRole is 1.

<!--- If Admin, drop a user to whom I say "bye",
and refresh my control frame --->
<cfif len(trim(AdminURL))
and (Statement is "bye")
and val(ToID)>
  <cfset Extra="MyLtr=A&TalkRole=1">
  <cfset DropID=val(ToID)>
  <cfinclude template="tudrop.cfm">
  <cfoutput>
    <body onload=
"javascript:parent.control.location.href='tucontrol.cfm?#Extra#'">
  </cfoutput>
</cfif>

Enter a New Statement

Now put code in tucontrol.cfm to provide controls for human help. Thus far, we've been following the preliminary action that occurs before the individual frames are loaded. Now, we'll look at the control frame that lets users take action.

If the letter isn't known, complain and stop. Include tuid.cfm to get the user and role. Set the query string. Include tuexdata.cfm to get external data. Set defaults.

The control frame submits form variables to itself. For administrators, it contains a select box named Pick. For the selected item, the value of Pick is two elements separated by an underscore: a list with the underscore as its delimiter. Element 1 is the letter of the session, and element 2 is the ID of the user to whom the administrator's statement is directed. (For other users, form.Pick contains only spaces separated by an underscore.) If Statement is not empty, and the trimmed value of Pick has data (not just the bare underscore), then address the statement in tblTalk to the letter and ID you've selected with Pick. (Only administrators have a pick list; all others take a default with spaces in the place of useful data.) If Statement is not empty, and the pick list does not have data, then address the statement in tblTalk to letter "A" (all administrators) and an ID of zero. Either way, if Statement is not empty, include tutalk.cfm to send it to the destination you've just set.

<!--- If url.MyLtr isn't available, stop --->
<cfif not (isDefined("url.MyLtr")
and len(trim(url.MyLtr)))>
  Talk letter missing; can't continue.
  <cfabort>
</cfif>

<!--- Get my ID, AdminURL, and parameters --->
<cfinclude template="tuid.cfm">

<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">

<!--- Get external data based on ID and role --->
<cfinclude template="tuexdata.cfm">

<!--- Set defaults --->
<cfparam name="form.Statement" default="">
<cfparam name="form.Pick" default=" _ ">

<!--- If statement isn't empty, send it --->
<cfif len(trim(form.Statement))>
  <cfif trim(form.Pick) is not "_">
    <cfset ToLtr=listGetAt(form.Pick,1,"_")>
    <cfset ToID=listGetAt(form.Pick,2,"_")>
  <cfelse>
    <cfset ToLtr="A">
    <cfset ToID=0>
  </cfif>
  <cfinclude template="tutalk.cfm">
</cfif>

Both administrators and other users need a form in the control frame. Have the form post back to this page, but add a query string (Extra) to the form action so the control frame will have the proper environment. Display the identity. If this is an administrator, show a pick list and force the page to focus on the pick list. Either way, the frame will have a statement field, a send button, and a close link.

Displayed identity consists of Company (or "Admin") and the individual's first name. For clarity during demonstrations, the ID is also displayed (in square brackets). For a finished application, you will want to change this so the ID is not displayed.

<!--- Accept input --->
<form name="Control" method="post" 
action=<cfoutput>"tucontrol.cfm?#Extra#"</cfoutput>>

<!--- Display my identity --->
<cfoutput>#Company#: #Name# [#MyID#]</cfoutput><br>

If this is an administrator - AdminURL isn't empty - query tblTalker for all help sessions (sequenced by help letter). The pick list will show three entries at a time but can hold a many as necessary. The value for each entry is expressed as {letter}_{ID}; the display for each entry is expressed as {letter} - {company}: {name}. If the ID for the entry is the current userid, then mark the entry "selected".

The statement text field is given a short size: 30 characters. This lets three help windows easily fit on the screen at once during demonstrations and lets administrators use just a small portion of their screens for human help. The field will hold 200 characters (by scrolling). So, the short field size doesn't limit conversation.

<!--- (Show pick list if admin) --->
<cfif len(trim(AdminURL))>
  <cfquery name="talkerList" datasource="context">
  select * from tblTalker
  order by TalkLtr
  </cfquery>
  <select name="Pick" size="3">
  <cfoutput query="talkerList">
  <cfif listGetAt(form.Pick,2,"_") is #Userid#>
    <cfset chosen="selected">
  <cfelse>
    <cfset chosen="">
  </cfif>
  <option value="#TalkLtr#_#Userid#" #chosen#>
  #TalkLtr# - #Company#: #Name#
  </cfoutput>
  </select>
  <br>
</cfif>
<input type="input" name="Statement" value=""
size="30" maxlength="200"><br>
<input type="submit" name="Send" value="Send">
(up to 200 characters)

The "Close" link uses a target of _top to break out of the frame with tu.cfm. Its query string, Extra, identifies the help session letter MyLtr (and for administrators, TalkRole=1). If this is an administrator, javascript shifts the browser's focus to the pick field. This allows administrators to type to users quickly. If the IE browser is used, the administrator can enter the letter of the user to receive the statement, tab once to the statement field, enter a statement, press {Enter}, and immediately repeat the cycle. If the Netscape browser is used, the cycle is almost as quick except that the administrator must use the cursor key instead of a session letter to pick the user to send a statement to. The javascript follows the classic pattern of hiding the actual script inside comments; although, without javascript, other aspects of this application won't work. You may wish to remove the comments, and you may wish to come up with javascript that makes things easier for administrators who use Netscape. It's useful for Netscape now; however, additional javascript could make it even better.

<cfoutput>
<a href="tu.cfm?Mode=bye&#Extra#" target="_top">Close</a>
</cfoutput>
</form>

<!--- If Admin, focus on pick --->
<cfif len(trim(AdminURL))>
  <script language="javascript">
  <!--
  document.Control.Pick.focus();
  // -->
  </script>
</cfif>

Show Conversation

Put code in tushow.cfm, the conversation frame; here's an overview of what it needs to do. If MyLtr isn't available, stop. For Administrators, list all conversations in progress, most recent statements first. The conversations will overlap, but the identifer that precedes each statement makes it possible to keep them straight. Regular users see only text they have sent or received. All users can scroll the conversation frame to look back if desired. The conversation frame also handles details related to closing a connection or alerting administrators that an individual has asked for help.

If MyLtr isn't already available - it should always be present - complain and stop. Otherwise, include tuid.cfm for identification, build a query string in the variable Extra, and list current conversations. For regular users, this list is filtered to include just text from and to the user and only includes statements that have not been retired. (Statements are retired when the conversation is over.)

<!--- If url.MyLtr isn't available, stop --->
<cfif not (isDefined("url.MyLtr")
and len(trim(url.MyLtr)))>
  Talk letter missing; can't continue.
  <cfabort>
</cfif>

<!--- Get my ID, Role, and parameters --->
<cfinclude template="tuid.cfm">

<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">

<!--- Get recent conversation, most recent first --->
<cfquery name="getRecent" datasource="context">
select * from tblTalk
<cfif not len(trim(AdminURL))>
  where Retired = 0
  and (ToID = #MyID# or FromID = #MyID#)
</cfif>
order by StatementKey desc
</cfquery>

If a user says "bye", include tudrop.cfm to drop the user from tblTalker and associated conversation from tuTalk. Then, use javascript to break out of the frame with tu.cfm, adding "bye" to the query string as if the user had clicked on the "Close" link. Tu.cfm will then finish the process of ending the help session.

<!--- If not Admin, drop myself when I say "bye",
and remove my frames --->
<cfif not len(trim(AdminURL))
and (trim(getRecent.Statement) is "bye")>
  <cfset DropID=val(MyID)>
  <cfinclude template="tudrop.cfm">
  <cfoutput>
  <body onload=
  "javascript:parent.location.href='tu.cfm?mode=bye&#Extra#'">
  </cfoutput>
</cfif>

If an administrator sees "bye" from someone else, include tudrop.cfm to drop that user. Then, reload the control frame so it won't display this user in the list of available help sessions.

<!--- If Admin, and most recent statement
is "bye" from someone else, drop that individual
and refresh the control frame --->
<cfif len(trim(AdminURL))
and (trim(getRecent.Statement) is "bye")
and (val(getRecent.FromID) is not MyID)>
  <cfset DropID=val(getRecent.FromID)>
  <cfinclude template="tudrop.cfm">
  <cfoutput>
  <body onload=
"javascript:parent.control.location.href='tucontrol.cfm?#Extra#'">
  </cfoutput>
</cfif>

If the most recent statement in an administrator's conversation frame is "paging...", change the background color to something attention-getting, and reload the control frame so its pick list will display the new request for help. The next statement that isn't "paging..." will shift the color back to normal. Non-admin users always have a normal color for the background; they won't see this effect.

You may wish to change the code to use a background sound instead of shifting the background color to get adminstrators' attention, but note first that most browsers are not configured properly to handle sound. Therefore, sound is not recommended for demonstrations of the human help technique. It takes time to get browsers to handle sound properly.

<!--- If Admin, and most recent statement is "paging...",
change the background color and refresh the control frame --->
<cfif len(trim(AdminURL))
and (trim(getRecent.Statement) is "paging...")>
  <cfoutput>
  <body bgcolor="BCBCBC" onload=
"javascript:parent.control.location.href='tucontrol.cfm?#Extra#'">
  </cfoutput>
<cfelse>
  <body bgcolor="FFFFFF">
</cfif>

Show statements starting with the most recent. The list is filtered as mentioned above. For regular users, each statement is preceded with "Admin" or with the user's name, either followed by a colon. For administrators, each statement is preceded with "{letter}-{name} to {letter}:" so it's clear who said what to whom. Statements from users usually include their names, and the names are also in the pick list. So, the fact that statements to users only show the letter to whom the statement is addressed isn't a handicap, just useful shorthand.

<!--- Show recent conversation --->
<cfoutput query="getRecent">
<cfif not len(trim(AdminURL))>
  <cfif trim(getRecent.FromLtr) is "A">
    Admin:
  <cfelse>
    #FromName#:
  </cfif>
<cfelse>
  #trim(getRecent.FromLtr)#-#trim(getRecent.FromName)#
  to #trim(getRecent.ToLtr)#:
</cfif>
#Statement#<br>
</cfoutput>

Drive

If the user had to press a button to see fresh text, the whole system would be less-than-satisfying. However, every 6 seconds, the driver frame checks for fresh text. It refreshes the conversation frame only when new text is available. Put the code for this function in tudrive.cfm.

If MyLtr isn't available - it should be - complain and stop. Include tuid.cfm to identify the user and role. Set Extra with MyLtr and AdminURL as usual. Read tblTalk to get the last statement key that's relevant to this user. If tblTalk is empty - it will be, initially - then add an empty row to the query and set the key value in that row to zero, just to have a starting point. "Last" in the URL usually contains the last relevant key seen by this frame. If the key from the table is greater than the key in the URL, then there are more statements to display; set More to 1. (If not, set More to zero.)

<!--- If url.MyLtr isn't available, stop --->
<cfif not ( isDefined("url.MyLtr")
and len(trim(url.MyLtr)) )>
  Talk letter missing; can't continue.
</cfif>

<!--- Get my ID, Role, and parameters --->
<cfinclude template="tuid.cfm">

<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">

<!--- Note last relevant statement key
and whether there are more than we've seen --->
<cfquery name="getLast" datasource="context">
select max(StatementKey) as Last from tblTalk
<cfif not len(trim(AdminURL))>
  where ToID = #val(MyID)#
  or FromID = #val(MyID)#
</cfif>
</cfquery>
<cfif not getLast.recordcount>
  <cfset dummy=queryAppendRow(getLast)>
</cfif>
<cfparam name="url.Last" default="0">
<cfif val(getLast.Last) gt url.Last>
  <cfset More=1>
<cfelse>
  <cfset More=0>
</cfif>

If your environment is like mine, you may develop on a non-secure server and publish on a secure server. A cfif statement can pick the right protocol automatically. If cgi.server_port isn't 443, then use http (unsecure) instead of https (secure). (cgi.server_protocol is a false lead; it's not the variable you want to track.)

Once you know the protocol, you can let the script location determine the rest of the information you need to write the URL for the driver frame. (You could hard-code the information, but then you'd have to maintain it manually.)

Add the query string. It consists of MyLtr, then the AdminURL, and then a Pulse that flip-flops between 1 and -1. Pulse is handy for demonstrations; because, you can change the code to enlarge the driver frame a little and then set its background color based on Pulse to dramatize its operation. For production, you probably don't need it.

Use meta refresh to load the resulting URL (with its query string) every six seconds. (You can play with this figure if you're happy with less-frequent updates.)

Once you've set up the refresh - something you have to do before entering the body of the frame - then if there's More data to display, use javascript to refresh the conversation frame. Open the body. Send trivial text (a break, for example). Before this break is a good place to put debug information while you're on the margin between test and production.

<!--- Before the body tag, set this URL to refresh --->
<cfif cgi.server_port does not contain "443">
  <cfset Protocol="http">
<cfelse>
  <cfset Protocol="https">
</cfif>
<cfset Myself="#Protocol#://">
<cfset Myself="#Myself##cgi.server_name##cgi.script_name#?">
<cfset Myself="#Myself#&Last=#val(getLast.Last)#">
<cfset Myself="#Myself#&MyLtr=#url.MyLtr##AdminURL#">
<cfif isDefined("url.Pulse") and (url.Pulse gt 0)>
  <cfset Pulse=-1>
<cfelse>
  <cfset Pulse=1>
</cfif>
<cfset Myself="#Myself#&Pulse=#Pulse#">
<cfoutput>

<meta http-equiv="refresh" content="6; URL=#Myself#">

<!--- Reload conversation if more --->
<cfif More>
  <cfset ConversationSrc="tushow.cfm?MyLtr=#url.MyLtr##AdminURL#">
  <body
onload="javascript:parent.conversation.location.href='#ConversationSrc#'">
<cfelse>
  <body>
</cfif>

</cfoutput>
<br>
</body>

Drop

When a help session must be ended, the user must dropped, and the conversation must be deleted or retired to avoid confusing users who ask for help a second time. Put the code for these functions in tudrop.cfm.

Look for the letter for this ID's help session in tblTalker. If found, then (with one exception) retire the statements to or from this ID. Statements consisting of the word "bye" are left active so that multiple windows will see them and get a chance to drop the talker from the active list.

<!--- Drop user and conversation --->
<cftransaction>
<cfquery name="talkerGet" datasource="context">
select TalkLtr from tblTalker
where Userid = #DropID#
</cfquery>
<cfif talkerGet.recordcount>
  <cfquery name="talkerDrop" datasource="context">
  <cfif cgi.server_name contains "127">
    delete * from tblTalker
  <cfelse>
    delete from tblTalker
  </cfif>
  where Userid = #val(DropID)#
  </cfquery>
  <cfquery name="talkDrop" datasource="context">
  update tblTalk
  set Retired = 1
  where (FromID = #val(DropID)# or ToID = #val(DropID)#)
  and Statement <> 'bye'
  </cfquery>
</cfif>
</cftransaction>

Try It

Most developers have at least two browsers from different families (e.g., Netscape and Microsoft) on their workstations for validating pages. Assuming your admin IDs are 4 and 5 and your vendor IDs are 118 and 120, use one browser to open an admin session in adminapp.cfm?ID=4, right-click to copy userapp.cfm?ID=118 into a different window of the same browser family to open a user session, and right-click to copy the following link - userapp.cfm?ID=120 - into a browser from a different family altogether to open a second user session. Click on the links they display so that you eventually get three human help windows open. Make them skinny and tall for best use of the screen.

Notice the background change in the admin window when any user window opens - even the admin window itself. Have a conversation with yourself. Click the "Close" link for a regular user; watch the admin window change and the regular user's frameset become a simple page. As admin, tell the other user "bye" and watch its session shut down.

Right-click and copy this link - adminapp.cfm?ID=5 - into a browser from the "other" family - the one you aren't already using as an administrator - and click the link it displays to get a second admin window open. Notice the how the existing admin window shows the new admin in the list. (Both are labelled "A" but display separate names). Browse userapp.cfm?ID=120 in a second window in either browser. Watch both admin windows change. Notice that both admin windows show all text. Notice that adminstrators can tell which admin is making which statements to the user but that the user only knows the statements come from "admin".

To make sure you've set things up right, highlight some of the conversation and watch it for a while. The highlight should not vanish after 6 seconds but should remain until the list of statements actually changes. Do the same for the control frame. If the highlight remains intact when nothing useful is happening, you've proven that the frame is only refreshed as necessary. (The driver frame refreshes periodically, but only the blur in the status line is visible.)

If you don't see a blur at all, there is probably an error in your driver frame. To see it, change tu.cfm to make the driver frame larger (or try to right-click on the last pixel of the frameset and view source).

Considerations

This capability has been approached in a number of ways over the years; here are considerations that went into this design. Having the most recent statement at the top of the list rather than at the bottom should simplify things for users of talking browsers someday; a listener will hear recent text first and can stop without having to review old statements over and over. Putting the user list in a select box instead of a raw list saves screen space. For IE users, using a select box also speeds selection; because, they can enter a single letter to jump to the user they want to reach. (Netscape users cursor down.) Giving letter labels to users speeds selection in this manner; non-admin users in active help sessions can be reached with one unique letter. Using one-letter labels also saves screen space when displaying statements; the letter is used in place of the user name and company when identifying the recipient of a statement. When it comes time to review a transcript - you'll hopefully archive old rows from tblTalk from time to time - you can use letters to quickly follow a conversation and IDs to identify the actual "speakers". The datetime of the most recent statement from a user is kept current to enable you to write additional code (later on) to drop non-communicative users.

This tip explores several advanced techniques to provide immediate interaction without a single line of Active X or Java. In the HTML realm, it uses targets in URLs, and it uses a pair of values (delimited by an underscore) instead of a single value for the data returned by a select box. It also manages a frameset, changes background color, and refreshes a frame. In the javascript realm, it sets field focus and lets one frame reload other frames. In the ColdFusion realm, it reminds us that there's a difference between putting a variable in ColdFusion's URL scope (in a cfset statement) and writing an actual URL to be seen by the Web server. It provides practice in acting on differences in the user environment. It sets cookies, works with databases, and modifies URLs. In short, this tip not only provides a valuable capability. Understanding it is a good springboard for other projects. =Marty=

[Instructions in the "Try It" section have been modified to make sure the user gets multiple windows open and to correct the ID=5 link.]